#
tokens: 46575/50000 7/296 files (page 9/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 9 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
│       │   ├── elm_language_server.py
│       │   ├── erlang_language_server.py
│       │   ├── gopls.py
│       │   ├── intelephense.py
│       │   ├── jedi_server.py
│       │   ├── kotlin_language_server.py
│       │   ├── lua_ls.py
│       │   ├── marksman.py
│       │   ├── nixd_ls.py
│       │   ├── omnisharp
│       │   │   ├── initialize_params.json
│       │   │   ├── runtime_dependencies.json
│       │   │   └── workspace_did_change_configuration.json
│       │   ├── omnisharp.py
│       │   ├── perl_language_server.py
│       │   ├── pyright_server.py
│       │   ├── r_language_server.py
│       │   ├── regal_server.py
│       │   ├── ruby_lsp.py
│       │   ├── rust_analyzer.py
│       │   ├── solargraph.py
│       │   ├── sourcekit_lsp.py
│       │   ├── terraform_ls.py
│       │   ├── typescript_language_server.py
│       │   ├── vts_language_server.py
│       │   └── zls.py
│       ├── ls_config.py
│       ├── ls_exceptions.py
│       ├── ls_handler.py
│       ├── ls_logger.py
│       ├── ls_request.py
│       ├── ls_types.py
│       ├── ls_utils.py
│       ├── ls.py
│       ├── lsp_protocol_handler
│       │   ├── lsp_constants.py
│       │   ├── lsp_requests.py
│       │   ├── lsp_types.py
│       │   └── server.py
│       ├── settings.py
│       └── util
│           ├── subprocess_util.py
│           └── zip.py
├── test
│   ├── __init__.py
│   ├── conftest.py
│   ├── resources
│   │   └── repos
│   │       ├── al
│   │       │   └── test_repo
│   │       │       ├── app.json
│   │       │       └── src
│   │       │           ├── Codeunits
│   │       │           │   ├── CustomerMgt.Codeunit.al
│   │       │           │   └── PaymentProcessorImpl.Codeunit.al
│   │       │           ├── Enums
│   │       │           │   └── CustomerType.Enum.al
│   │       │           ├── Interfaces
│   │       │           │   └── IPaymentProcessor.Interface.al
│   │       │           ├── Pages
│   │       │           │   ├── CustomerCard.Page.al
│   │       │           │   └── CustomerList.Page.al
│   │       │           ├── TableExtensions
│   │       │           │   └── Item.TableExt.al
│   │       │           └── Tables
│   │       │               └── Customer.Table.al
│   │       ├── bash
│   │       │   └── test_repo
│   │       │       ├── config.sh
│   │       │       ├── main.sh
│   │       │       └── utils.sh
│   │       ├── clojure
│   │       │   └── test_repo
│   │       │       ├── deps.edn
│   │       │       └── src
│   │       │           └── test_app
│   │       │               ├── core.clj
│   │       │               └── utils.clj
│   │       ├── csharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Models
│   │       │       │   └── Person.cs
│   │       │       ├── Program.cs
│   │       │       ├── serena.sln
│   │       │       └── TestProject.csproj
│   │       ├── dart
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── helper.dart
│   │       │       │   ├── main.dart
│   │       │       │   └── models.dart
│   │       │       └── pubspec.yaml
│   │       ├── elixir
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── examples.ex
│   │       │       │   ├── ignored_dir
│   │       │       │   │   └── ignored_module.ex
│   │       │       │   ├── models.ex
│   │       │       │   ├── services.ex
│   │       │       │   ├── test_repo.ex
│   │       │       │   └── utils.ex
│   │       │       ├── mix.exs
│   │       │       ├── mix.lock
│   │       │       ├── scripts
│   │       │       │   └── build_script.ex
│   │       │       └── test
│   │       │           ├── models_test.exs
│   │       │           └── test_repo_test.exs
│   │       ├── elm
│   │       │   └── test_repo
│   │       │       ├── elm.json
│   │       │       ├── Main.elm
│   │       │       └── Utils.elm
│   │       ├── erlang
│   │       │   └── test_repo
│   │       │       ├── hello.erl
│   │       │       ├── ignored_dir
│   │       │       │   └── ignored_module.erl
│   │       │       ├── include
│   │       │       │   ├── records.hrl
│   │       │       │   └── types.hrl
│   │       │       ├── math_utils.erl
│   │       │       ├── rebar.config
│   │       │       ├── src
│   │       │       │   ├── app.erl
│   │       │       │   ├── models.erl
│   │       │       │   ├── services.erl
│   │       │       │   └── utils.erl
│   │       │       └── test
│   │       │           ├── models_tests.erl
│   │       │           └── utils_tests.erl
│   │       ├── go
│   │       │   └── test_repo
│   │       │       └── main.go
│   │       ├── java
│   │       │   └── test_repo
│   │       │       ├── pom.xml
│   │       │       └── src
│   │       │           └── main
│   │       │               └── java
│   │       │                   └── test_repo
│   │       │                       ├── Main.java
│   │       │                       ├── Model.java
│   │       │                       ├── ModelUser.java
│   │       │                       └── Utils.java
│   │       ├── kotlin
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle.kts
│   │       │       └── src
│   │       │           └── main
│   │       │               └── kotlin
│   │       │                   └── test_repo
│   │       │                       ├── Main.kt
│   │       │                       ├── Model.kt
│   │       │                       ├── ModelUser.kt
│   │       │                       └── Utils.kt
│   │       ├── lua
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── main.lua
│   │       │       ├── src
│   │       │       │   ├── calculator.lua
│   │       │       │   └── utils.lua
│   │       │       └── tests
│   │       │           └── test_calculator.lua
│   │       ├── markdown
│   │       │   └── test_repo
│   │       │       ├── api.md
│   │       │       ├── CONTRIBUTING.md
│   │       │       ├── guide.md
│   │       │       └── README.md
│   │       ├── nix
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── default.nix
│   │       │       ├── flake.nix
│   │       │       ├── lib
│   │       │       │   └── utils.nix
│   │       │       ├── modules
│   │       │       │   └── example.nix
│   │       │       └── scripts
│   │       │           └── hello.sh
│   │       ├── perl
│   │       │   └── test_repo
│   │       │       ├── helper.pl
│   │       │       └── main.pl
│   │       ├── php
│   │       │   └── test_repo
│   │       │       ├── helper.php
│   │       │       ├── index.php
│   │       │       └── simple_var.php
│   │       ├── python
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── custom_test
│   │       │       │   ├── __init__.py
│   │       │       │   └── advanced_features.py
│   │       │       ├── examples
│   │       │       │   ├── __init__.py
│   │       │       │   └── user_management.py
│   │       │       ├── ignore_this_dir_with_postfix
│   │       │       │   └── ignored_module.py
│   │       │       ├── scripts
│   │       │       │   ├── __init__.py
│   │       │       │   └── run_app.py
│   │       │       └── test_repo
│   │       │           ├── __init__.py
│   │       │           ├── complex_types.py
│   │       │           ├── models.py
│   │       │           ├── name_collisions.py
│   │       │           ├── nested_base.py
│   │       │           ├── nested.py
│   │       │           ├── overloaded.py
│   │       │           ├── services.py
│   │       │           ├── utils.py
│   │       │           └── variables.py
│   │       ├── r
│   │       │   └── test_repo
│   │       │       ├── .Rbuildignore
│   │       │       ├── DESCRIPTION
│   │       │       ├── examples
│   │       │       │   └── analysis.R
│   │       │       ├── NAMESPACE
│   │       │       └── R
│   │       │           ├── models.R
│   │       │           └── utils.R
│   │       ├── rego
│   │       │   └── test_repo
│   │       │       ├── policies
│   │       │       │   ├── authz.rego
│   │       │       │   └── validation.rego
│   │       │       └── utils
│   │       │           └── helpers.rego
│   │       ├── ruby
│   │       │   └── test_repo
│   │       │       ├── .solargraph.yml
│   │       │       ├── examples
│   │       │       │   └── user_management.rb
│   │       │       ├── lib.rb
│   │       │       ├── main.rb
│   │       │       ├── models.rb
│   │       │       ├── nested.rb
│   │       │       ├── services.rb
│   │       │       └── variables.rb
│   │       ├── rust
│   │       │   ├── test_repo
│   │       │   │   ├── Cargo.lock
│   │       │   │   ├── Cargo.toml
│   │       │   │   └── src
│   │       │   │       ├── lib.rs
│   │       │   │       └── main.rs
│   │       │   └── test_repo_2024
│   │       │       ├── Cargo.lock
│   │       │       ├── Cargo.toml
│   │       │       └── src
│   │       │           ├── lib.rs
│   │       │           └── main.rs
│   │       ├── swift
│   │       │   └── test_repo
│   │       │       ├── Package.swift
│   │       │       └── src
│   │       │           ├── main.swift
│   │       │           └── utils.swift
│   │       ├── terraform
│   │       │   └── test_repo
│   │       │       ├── data.tf
│   │       │       ├── main.tf
│   │       │       ├── outputs.tf
│   │       │       └── variables.tf
│   │       ├── typescript
│   │       │   └── test_repo
│   │       │       ├── .serena
│   │       │       │   └── project.yml
│   │       │       ├── index.ts
│   │       │       ├── tsconfig.json
│   │       │       └── use_helper.ts
│   │       └── zig
│   │           └── test_repo
│   │               ├── .gitignore
│   │               ├── build.zig
│   │               ├── src
│   │               │   ├── calculator.zig
│   │               │   ├── main.zig
│   │               │   └── math_utils.zig
│   │               └── zls.json
│   ├── serena
│   │   ├── __init__.py
│   │   ├── __snapshots__
│   │   │   └── test_symbol_editing.ambr
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   └── test_serena_config.py
│   │   ├── test_edit_marker.py
│   │   ├── test_mcp.py
│   │   ├── test_serena_agent.py
│   │   ├── test_symbol_editing.py
│   │   ├── test_symbol.py
│   │   ├── test_text_utils.py
│   │   ├── test_tool_parameter_types.py
│   │   └── util
│   │       ├── test_exception.py
│   │       └── test_file_system.py
│   └── solidlsp
│       ├── al
│       │   └── test_al_basic.py
│       ├── bash
│       │   ├── __init__.py
│       │   └── test_bash_basic.py
│       ├── clojure
│       │   ├── __init__.py
│       │   └── test_clojure_basic.py
│       ├── csharp
│       │   └── test_csharp_basic.py
│       ├── dart
│       │   ├── __init__.py
│       │   └── test_dart_basic.py
│       ├── elixir
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_elixir_basic.py
│       │   ├── test_elixir_ignored_dirs.py
│       │   ├── test_elixir_integration.py
│       │   └── test_elixir_symbol_retrieval.py
│       ├── elm
│       │   └── test_elm_basic.py
│       ├── erlang
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_erlang_basic.py
│       │   ├── test_erlang_ignored_dirs.py
│       │   └── test_erlang_symbol_retrieval.py
│       ├── go
│       │   └── test_go_basic.py
│       ├── java
│       │   └── test_java_basic.py
│       ├── kotlin
│       │   └── test_kotlin_basic.py
│       ├── lua
│       │   └── test_lua_basic.py
│       ├── markdown
│       │   ├── __init__.py
│       │   └── test_markdown_basic.py
│       ├── nix
│       │   └── test_nix_basic.py
│       ├── perl
│       │   └── test_perl_basic.py
│       ├── php
│       │   └── test_php_basic.py
│       ├── python
│       │   ├── test_python_basic.py
│       │   ├── test_retrieval_with_ignored_dirs.py
│       │   └── test_symbol_retrieval.py
│       ├── r
│       │   ├── __init__.py
│       │   └── test_r_basic.py
│       ├── rego
│       │   └── test_rego_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

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/omnisharp/initialize_params.json:
--------------------------------------------------------------------------------

```json
  1 | {
  2 |     "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
  3 |     "processId": "os.getpid()",
  4 |     "clientInfo": {
  5 |         "name": "Visual Studio Code - Insiders",
  6 |         "version": "1.82.0-insider"
  7 |     },
  8 |     "locale": "en",
  9 |     "rootPath": "$rootPath",
 10 |     "rootUri": "$rootUri",
 11 |     "capabilities": {
 12 |         "workspace": {
 13 |             "applyEdit": true,
 14 |             "workspaceEdit": {
 15 |                 "documentChanges": true,
 16 |                 "resourceOperations": [
 17 |                     "create",
 18 |                     "rename",
 19 |                     "delete"
 20 |                 ],
 21 |                 "failureHandling": "textOnlyTransactional",
 22 |                 "normalizesLineEndings": true,
 23 |                 "changeAnnotationSupport": {
 24 |                     "groupsOnLabel": true
 25 |                 }
 26 |             },
 27 |             "configuration": false,
 28 |             "didChangeWatchedFiles": {
 29 |                 "dynamicRegistration": true,
 30 |                 "relativePatternSupport": true
 31 |             },
 32 |             "symbol": {
 33 |                 "dynamicRegistration": true,
 34 |                 "symbolKind": {
 35 |                     "valueSet": [
 36 |                         1,
 37 |                         2,
 38 |                         3,
 39 |                         4,
 40 |                         5,
 41 |                         6,
 42 |                         7,
 43 |                         8,
 44 |                         9,
 45 |                         10,
 46 |                         11,
 47 |                         12,
 48 |                         13,
 49 |                         14,
 50 |                         15,
 51 |                         16,
 52 |                         17,
 53 |                         18,
 54 |                         19,
 55 |                         20,
 56 |                         21,
 57 |                         22,
 58 |                         23,
 59 |                         24,
 60 |                         25,
 61 |                         26
 62 |                     ]
 63 |                 },
 64 |                 "tagSupport": {
 65 |                     "valueSet": [
 66 |                         1
 67 |                     ]
 68 |                 },
 69 |                 "resolveSupport": {
 70 |                     "properties": [
 71 |                         "location.range"
 72 |                     ]
 73 |                 }
 74 |             },
 75 |             "codeLens": {
 76 |                 "refreshSupport": true
 77 |             },
 78 |             "executeCommand": {
 79 |                 "dynamicRegistration": true
 80 |             },
 81 |             "didChangeConfiguration": {
 82 |                 "dynamicRegistration": true
 83 |             },
 84 |             "workspaceFolders": true,
 85 |             "semanticTokens": {
 86 |                 "refreshSupport": true
 87 |             },
 88 |             "fileOperations": {
 89 |                 "dynamicRegistration": true,
 90 |                 "didCreate": true,
 91 |                 "didRename": true,
 92 |                 "didDelete": true,
 93 |                 "willCreate": true,
 94 |                 "willRename": true,
 95 |                 "willDelete": true
 96 |             },
 97 |             "inlineValue": {
 98 |                 "refreshSupport": true
 99 |             },
100 |             "inlayHint": {
101 |                 "refreshSupport": true
102 |             },
103 |             "diagnostics": {
104 |                 "refreshSupport": true
105 |             }
106 |         },
107 |         "textDocument": {
108 |             "publishDiagnostics": {
109 |                 "relatedInformation": true,
110 |                 "versionSupport": false,
111 |                 "tagSupport": {
112 |                     "valueSet": [
113 |                         1,
114 |                         2
115 |                     ]
116 |                 },
117 |                 "codeDescriptionSupport": true,
118 |                 "dataSupport": true
119 |             },
120 |             "synchronization": {
121 |                 "dynamicRegistration": true,
122 |                 "willSave": true,
123 |                 "willSaveWaitUntil": true,
124 |                 "didSave": true
125 |             },
126 |             "completion": {
127 |                 "dynamicRegistration": true,
128 |                 "contextSupport": true,
129 |                 "completionItem": {
130 |                     "snippetSupport": true,
131 |                     "commitCharactersSupport": true,
132 |                     "documentationFormat": [
133 |                         "markdown",
134 |                         "plaintext"
135 |                     ],
136 |                     "deprecatedSupport": true,
137 |                     "preselectSupport": true,
138 |                     "tagSupport": {
139 |                         "valueSet": [
140 |                             1
141 |                         ]
142 |                     },
143 |                     "insertReplaceSupport": true,
144 |                     "resolveSupport": {
145 |                         "properties": [
146 |                             "documentation",
147 |                             "detail",
148 |                             "additionalTextEdits"
149 |                         ]
150 |                     },
151 |                     "insertTextModeSupport": {
152 |                         "valueSet": [
153 |                             1,
154 |                             2
155 |                         ]
156 |                     },
157 |                     "labelDetailsSupport": true
158 |                 },
159 |                 "insertTextMode": 2,
160 |                 "completionItemKind": {
161 |                     "valueSet": [
162 |                         1,
163 |                         2,
164 |                         3,
165 |                         4,
166 |                         5,
167 |                         6,
168 |                         7,
169 |                         8,
170 |                         9,
171 |                         10,
172 |                         11,
173 |                         12,
174 |                         13,
175 |                         14,
176 |                         16,
177 |                         17,
178 |                         18,
179 |                         19,
180 |                         20,
181 |                         21,
182 |                         22,
183 |                         23,
184 |                         24,
185 |                         25
186 |                     ]
187 |                 },
188 |                 "completionList": {
189 |                     "itemDefaults": [
190 |                         "commitCharacters",
191 |                         "editRange",
192 |                         "insertTextFormat",
193 |                         "insertTextMode"
194 |                     ]
195 |                 }
196 |             },
197 |             "hover": {
198 |                 "dynamicRegistration": true,
199 |                 "contentFormat": [
200 |                     "markdown",
201 |                     "plaintext"
202 |                 ]
203 |             },
204 |             "signatureHelp": {
205 |                 "dynamicRegistration": true,
206 |                 "signatureInformation": {
207 |                     "documentationFormat": [
208 |                         "markdown",
209 |                         "plaintext"
210 |                     ],
211 |                     "parameterInformation": {
212 |                         "labelOffsetSupport": true
213 |                     },
214 |                     "activeParameterSupport": true
215 |                 },
216 |                 "contextSupport": true
217 |             },
218 |             "definition": {
219 |                 "dynamicRegistration": true,
220 |                 "linkSupport": true
221 |             },
222 |             "references": {
223 |                 "dynamicRegistration": true
224 |             },
225 |             "documentHighlight": {
226 |                 "dynamicRegistration": true
227 |             },
228 |             "documentSymbol": {
229 |                 "dynamicRegistration": true,
230 |                 "symbolKind": {
231 |                     "valueSet": [
232 |                         1,
233 |                         2,
234 |                         3,
235 |                         4,
236 |                         5,
237 |                         6,
238 |                         7,
239 |                         8,
240 |                         9,
241 |                         10,
242 |                         11,
243 |                         12,
244 |                         13,
245 |                         14,
246 |                         15,
247 |                         16,
248 |                         17,
249 |                         18,
250 |                         19,
251 |                         20,
252 |                         21,
253 |                         22,
254 |                         23,
255 |                         24,
256 |                         25,
257 |                         26
258 |                     ]
259 |                 },
260 |                 "hierarchicalDocumentSymbolSupport": true,
261 |                 "tagSupport": {
262 |                     "valueSet": [
263 |                         1
264 |                     ]
265 |                 },
266 |                 "labelSupport": true
267 |             },
268 |             "codeAction": {
269 |                 "dynamicRegistration": true,
270 |                 "isPreferredSupport": true,
271 |                 "disabledSupport": true,
272 |                 "dataSupport": true,
273 |                 "resolveSupport": {
274 |                     "properties": [
275 |                         "edit"
276 |                     ]
277 |                 },
278 |                 "codeActionLiteralSupport": {
279 |                     "codeActionKind": {
280 |                         "valueSet": [
281 |                             "",
282 |                             "quickfix",
283 |                             "refactor",
284 |                             "refactor.extract",
285 |                             "refactor.inline",
286 |                             "refactor.rewrite",
287 |                             "source",
288 |                             "source.organizeImports"
289 |                         ]
290 |                     }
291 |                 },
292 |                 "honorsChangeAnnotations": false
293 |             },
294 |             "codeLens": {
295 |                 "dynamicRegistration": true
296 |             },
297 |             "formatting": {
298 |                 "dynamicRegistration": true
299 |             },
300 |             "rangeFormatting": {
301 |                 "dynamicRegistration": true
302 |             },
303 |             "onTypeFormatting": {
304 |                 "dynamicRegistration": true
305 |             },
306 |             "rename": {
307 |                 "dynamicRegistration": true,
308 |                 "prepareSupport": true,
309 |                 "prepareSupportDefaultBehavior": 1,
310 |                 "honorsChangeAnnotations": true
311 |             },
312 |             "documentLink": {
313 |                 "dynamicRegistration": true,
314 |                 "tooltipSupport": true
315 |             },
316 |             "typeDefinition": {
317 |                 "dynamicRegistration": true,
318 |                 "linkSupport": true
319 |             },
320 |             "implementation": {
321 |                 "dynamicRegistration": true,
322 |                 "linkSupport": true
323 |             },
324 |             "colorProvider": {
325 |                 "dynamicRegistration": true
326 |             },
327 |             "foldingRange": {
328 |                 "dynamicRegistration": true,
329 |                 "rangeLimit": 5000,
330 |                 "lineFoldingOnly": true,
331 |                 "foldingRangeKind": {
332 |                     "valueSet": [
333 |                         "comment",
334 |                         "imports",
335 |                         "region"
336 |                     ]
337 |                 },
338 |                 "foldingRange": {
339 |                     "collapsedText": false
340 |                 }
341 |             },
342 |             "declaration": {
343 |                 "dynamicRegistration": true,
344 |                 "linkSupport": true
345 |             },
346 |             "selectionRange": {
347 |                 "dynamicRegistration": true
348 |             },
349 |             "callHierarchy": {
350 |                 "dynamicRegistration": true
351 |             },
352 |             "semanticTokens": {
353 |                 "dynamicRegistration": true,
354 |                 "tokenTypes": [
355 |                     "namespace",
356 |                     "type",
357 |                     "class",
358 |                     "enum",
359 |                     "interface",
360 |                     "struct",
361 |                     "typeParameter",
362 |                     "parameter",
363 |                     "variable",
364 |                     "property",
365 |                     "enumMember",
366 |                     "event",
367 |                     "function",
368 |                     "method",
369 |                     "macro",
370 |                     "keyword",
371 |                     "modifier",
372 |                     "comment",
373 |                     "string",
374 |                     "number",
375 |                     "regexp",
376 |                     "operator",
377 |                     "decorator"
378 |                 ],
379 |                 "tokenModifiers": [
380 |                     "declaration",
381 |                     "definition",
382 |                     "readonly",
383 |                     "static",
384 |                     "deprecated",
385 |                     "abstract",
386 |                     "async",
387 |                     "modification",
388 |                     "documentation",
389 |                     "defaultLibrary"
390 |                 ],
391 |                 "formats": [
392 |                     "relative"
393 |                 ],
394 |                 "requests": {
395 |                     "range": true,
396 |                     "full": {
397 |                         "delta": true
398 |                     }
399 |                 },
400 |                 "multilineTokenSupport": false,
401 |                 "overlappingTokenSupport": false,
402 |                 "serverCancelSupport": true,
403 |                 "augmentsSyntaxTokens": false
404 |             },
405 |             "linkedEditingRange": {
406 |                 "dynamicRegistration": true
407 |             },
408 |             "typeHierarchy": {
409 |                 "dynamicRegistration": true
410 |             },
411 |             "inlineValue": {
412 |                 "dynamicRegistration": true
413 |             },
414 |             "inlayHint": {
415 |                 "dynamicRegistration": true,
416 |                 "resolveSupport": {
417 |                     "properties": [
418 |                         "tooltip",
419 |                         "textEdits",
420 |                         "label.tooltip",
421 |                         "label.location",
422 |                         "label.command"
423 |                     ]
424 |                 }
425 |             },
426 |             "diagnostic": {
427 |                 "dynamicRegistration": true,
428 |                 "relatedDocumentSupport": false
429 |             }
430 |         },
431 |         "window": {
432 |             "showMessage": {
433 |                 "messageActionItem": {
434 |                     "additionalPropertiesSupport": true
435 |                 }
436 |             },
437 |             "showDocument": {
438 |                 "support": true
439 |             },
440 |             "workDoneProgress": true
441 |         },
442 |         "general": {
443 |             "staleRequestSupport": {
444 |                 "cancel": true,
445 |                 "retryOnContentModified": [
446 |                     "textDocument/semanticTokens/full",
447 |                     "textDocument/semanticTokens/range",
448 |                     "textDocument/semanticTokens/full/delta"
449 |                 ]
450 |             },
451 |             "regularExpressions": {
452 |                 "engine": "ECMAScript",
453 |                 "version": "ES2020"
454 |             },
455 |             "markdown": {
456 |                 "parser": "marked",
457 |                 "version": "1.1.0",
458 |                 "allowedTags": [
459 |                     "ul",
460 |                     "li",
461 |                     "p",
462 |                     "code",
463 |                     "blockquote",
464 |                     "ol",
465 |                     "h1",
466 |                     "h2",
467 |                     "h3",
468 |                     "h4",
469 |                     "h5",
470 |                     "h6",
471 |                     "hr",
472 |                     "em",
473 |                     "pre",
474 |                     "table",
475 |                     "thead",
476 |                     "tbody",
477 |                     "tr",
478 |                     "th",
479 |                     "td",
480 |                     "div",
481 |                     "del",
482 |                     "a",
483 |                     "strong",
484 |                     "br",
485 |                     "img",
486 |                     "span"
487 |                 ]
488 |             },
489 |             "positionEncodings": [
490 |                 "utf-16"
491 |             ]
492 |         },
493 |         "notebookDocument": {
494 |             "synchronization": {
495 |                 "dynamicRegistration": true,
496 |                 "executionSummarySupport": true
497 |             }
498 |         },
499 |         "experimental": {
500 |             "snippetTextEdit": true,
501 |             "codeActionGroup": true,
502 |             "hoverActions": true,
503 |             "serverStatusNotification": true,
504 |             "colorDiagnosticOutput": true,
505 |             "openServerLogs": true,
506 |             "commands": {
507 |                 "commands": [
508 |                     "editor.action.triggerParameterHints"
509 |                 ]
510 |             }
511 |         }
512 |     },
513 |     "initializationOptions": {
514 |         "RoslynExtensionsOptions": {
515 |             "EnableDecompilationSupport": false,
516 |             "EnableAnalyzersSupport": true,
517 |             "EnableImportCompletion": true,
518 |             "EnableAsyncCompletion": false,
519 |             "DocumentAnalysisTimeoutMs": 30000,
520 |             "DiagnosticWorkersThreadCount": 18,
521 |             "AnalyzeOpenDocumentsOnly": true,
522 |             "InlayHintsOptions": {
523 |                 "EnableForParameters": false,
524 |                 "ForLiteralParameters": false,
525 |                 "ForIndexerParameters": false,
526 |                 "ForObjectCreationParameters": false,
527 |                 "ForOtherParameters": false,
528 |                 "SuppressForParametersThatDifferOnlyBySuffix": false,
529 |                 "SuppressForParametersThatMatchMethodIntent": false,
530 |                 "SuppressForParametersThatMatchArgumentName": false,
531 |                 "EnableForTypes": false,
532 |                 "ForImplicitVariableTypes": false,
533 |                 "ForLambdaParameterTypes": false,
534 |                 "ForImplicitObjectCreation": false
535 |             },
536 |             "LocationPaths": null
537 |         },
538 |         "FormattingOptions": {
539 |             "OrganizeImports": false,
540 |             "EnableEditorConfigSupport": true,
541 |             "NewLine": "\n",
542 |             "UseTabs": false,
543 |             "TabSize": 4,
544 |             "IndentationSize": 4,
545 |             "SpacingAfterMethodDeclarationName": false,
546 |             "SeparateImportDirectiveGroups": false,
547 |             "SpaceWithinMethodDeclarationParenthesis": false,
548 |             "SpaceBetweenEmptyMethodDeclarationParentheses": false,
549 |             "SpaceAfterMethodCallName": false,
550 |             "SpaceWithinMethodCallParentheses": false,
551 |             "SpaceBetweenEmptyMethodCallParentheses": false,
552 |             "SpaceAfterControlFlowStatementKeyword": true,
553 |             "SpaceWithinExpressionParentheses": false,
554 |             "SpaceWithinCastParentheses": false,
555 |             "SpaceWithinOtherParentheses": false,
556 |             "SpaceAfterCast": false,
557 |             "SpaceBeforeOpenSquareBracket": false,
558 |             "SpaceBetweenEmptySquareBrackets": false,
559 |             "SpaceWithinSquareBrackets": false,
560 |             "SpaceAfterColonInBaseTypeDeclaration": true,
561 |             "SpaceAfterComma": true,
562 |             "SpaceAfterDot": false,
563 |             "SpaceAfterSemicolonsInForStatement": true,
564 |             "SpaceBeforeColonInBaseTypeDeclaration": true,
565 |             "SpaceBeforeComma": false,
566 |             "SpaceBeforeDot": false,
567 |             "SpaceBeforeSemicolonsInForStatement": false,
568 |             "SpacingAroundBinaryOperator": "single",
569 |             "IndentBraces": false,
570 |             "IndentBlock": true,
571 |             "IndentSwitchSection": true,
572 |             "IndentSwitchCaseSection": true,
573 |             "IndentSwitchCaseSectionWhenBlock": true,
574 |             "LabelPositioning": "oneLess",
575 |             "WrappingPreserveSingleLine": true,
576 |             "WrappingKeepStatementsOnSingleLine": true,
577 |             "NewLinesForBracesInTypes": true,
578 |             "NewLinesForBracesInMethods": true,
579 |             "NewLinesForBracesInProperties": true,
580 |             "NewLinesForBracesInAccessors": true,
581 |             "NewLinesForBracesInAnonymousMethods": true,
582 |             "NewLinesForBracesInControlBlocks": true,
583 |             "NewLinesForBracesInAnonymousTypes": true,
584 |             "NewLinesForBracesInObjectCollectionArrayInitializers": true,
585 |             "NewLinesForBracesInLambdaExpressionBody": true,
586 |             "NewLineForElse": true,
587 |             "NewLineForCatch": true,
588 |             "NewLineForFinally": true,
589 |             "NewLineForMembersInObjectInit": true,
590 |             "NewLineForMembersInAnonymousTypes": true,
591 |             "NewLineForClausesInQuery": true
592 |         },
593 |         "FileOptions": {
594 |             "SystemExcludeSearchPatterns": [
595 |                 "**/node_modules/**/*",
596 |                 "**/bin/**/*",
597 |                 "**/obj/**/*",
598 |                 "**/.git/**/*",
599 |                 "**/.git",
600 |                 "**/.svn",
601 |                 "**/.hg",
602 |                 "**/CVS",
603 |                 "**/.DS_Store",
604 |                 "**/Thumbs.db"
605 |             ],
606 |             "ExcludeSearchPatterns": []
607 |         },
608 |         "RenameOptions": {
609 |             "RenameOverloads": false,
610 |             "RenameInStrings": false,
611 |             "RenameInComments": false
612 |         },
613 |         "ImplementTypeOptions": {
614 |             "InsertionBehavior": 0,
615 |             "PropertyGenerationBehavior": 0
616 |         },
617 |         "DotNetCliOptions": {
618 |             "LocationPaths": null
619 |         },
620 |         "Plugins": {
621 |             "LocationPaths": null
622 |         }
623 |     },
624 |     "trace": "verbose",
625 |     "workspaceFolders": [
626 |         {
627 |             "uri": "$uri",
628 |             "name": "$name"
629 |         }
630 |     ]
631 | }
```

--------------------------------------------------------------------------------
/test/solidlsp/erlang/test_erlang_symbol_retrieval.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the Erlang 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 | """
  9 | 
 10 | import os
 11 | 
 12 | import pytest
 13 | 
 14 | from solidlsp import SolidLanguageServer
 15 | from solidlsp.ls_config import Language
 16 | from solidlsp.ls_types import SymbolKind
 17 | 
 18 | from . import ERLANG_LS_UNAVAILABLE, ERLANG_LS_UNAVAILABLE_REASON
 19 | 
 20 | # These marks will be applied to all tests in this module
 21 | pytestmark = [
 22 |     pytest.mark.erlang,
 23 |     pytest.mark.skipif(ERLANG_LS_UNAVAILABLE, reason=f"Erlang LS not available: {ERLANG_LS_UNAVAILABLE_REASON}"),
 24 | ]
 25 | 
 26 | 
 27 | class TestErlangLanguageServerSymbols:
 28 |     """Test the Erlang language server's symbol-related functionality."""
 29 | 
 30 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
 31 |     def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
 32 |         """Test request_containing_symbol for a function."""
 33 |         # Test for a position inside the create_user function
 34 |         file_path = os.path.join("src", "models.erl")
 35 | 
 36 |         # Find the create_user function in the file
 37 |         content = language_server.retrieve_full_file_content(file_path)
 38 |         lines = content.split("\n")
 39 |         create_user_line = None
 40 |         for i, line in enumerate(lines):
 41 |             if "create_user(" in line and "-spec" not in line:
 42 |                 create_user_line = i + 1  # Go inside the function body
 43 |                 break
 44 | 
 45 |         if create_user_line is None:
 46 |             pytest.skip("Could not find create_user function")
 47 | 
 48 |         containing_symbol = language_server.request_containing_symbol(file_path, create_user_line, 10, include_body=True)
 49 | 
 50 |         # Verify that we found the containing symbol
 51 |         if containing_symbol:
 52 |             assert "create_user" in containing_symbol["name"]
 53 |             assert containing_symbol["kind"] == SymbolKind.Method or containing_symbol["kind"] == SymbolKind.Function
 54 |             if "body" in containing_symbol:
 55 |                 assert "create_user" in containing_symbol["body"]
 56 | 
 57 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
 58 |     def test_request_containing_symbol_module(self, language_server: SolidLanguageServer) -> None:
 59 |         """Test request_containing_symbol for a module."""
 60 |         # Test for a position inside the models module but outside any function
 61 |         file_path = os.path.join("src", "models.erl")
 62 | 
 63 |         # Find the module definition
 64 |         content = language_server.retrieve_full_file_content(file_path)
 65 |         lines = content.split("\n")
 66 |         module_line = None
 67 |         for i, line in enumerate(lines):
 68 |             if "-module(models)" in line:
 69 |                 module_line = i + 2  # Go inside the module
 70 |                 break
 71 | 
 72 |         if module_line is None:
 73 |             pytest.skip("Could not find models module")
 74 | 
 75 |         containing_symbol = language_server.request_containing_symbol(file_path, module_line, 5)
 76 | 
 77 |         # Verify that we found the containing symbol
 78 |         if containing_symbol:
 79 |             assert "models" in containing_symbol["name"] or "module" in containing_symbol["name"].lower()
 80 |             assert containing_symbol["kind"] == SymbolKind.Module or containing_symbol["kind"] == SymbolKind.Class
 81 | 
 82 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
 83 |     def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
 84 |         """Test request_containing_symbol with nested scopes."""
 85 |         # Test for a position inside a function which is inside a module
 86 |         file_path = os.path.join("src", "models.erl")
 87 | 
 88 |         # Find a function inside models module
 89 |         content = language_server.retrieve_full_file_content(file_path)
 90 |         lines = content.split("\n")
 91 |         function_body_line = None
 92 |         for i, line in enumerate(lines):
 93 |             if "create_user(" in line and "-spec" not in line:
 94 |                 # Go deeper into the function body where there might be case expressions
 95 |                 for j in range(i + 1, min(i + 10, len(lines))):
 96 |                     if lines[j].strip() and not lines[j].strip().startswith("%"):
 97 |                         function_body_line = j
 98 |                         break
 99 |                 break
100 | 
101 |         if function_body_line is None:
102 |             pytest.skip("Could not find function body")
103 | 
104 |         containing_symbol = language_server.request_containing_symbol(file_path, function_body_line, 15)
105 | 
106 |         # Verify that we found the innermost containing symbol (the function)
107 |         if containing_symbol:
108 |             expected_names = ["create_user", "models"]
109 |             assert any(name in containing_symbol["name"] for name in expected_names)
110 | 
111 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
112 |     def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
113 |         """Test request_containing_symbol for a position with no containing symbol."""
114 |         # Test for a position outside any function/module (e.g., in comments)
115 |         file_path = os.path.join("src", "models.erl")
116 |         # Line 1-2 are likely module declaration or comments
117 |         containing_symbol = language_server.request_containing_symbol(file_path, 2, 10)
118 | 
119 |         # Should return None or an empty dictionary, or the top-level module
120 |         # This is acceptable behavior for module-level positions
121 |         assert containing_symbol is None or containing_symbol == {} or "models" in str(containing_symbol)
122 | 
123 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
124 |     def test_request_referencing_symbols_record(self, language_server: SolidLanguageServer) -> None:
125 |         """Test request_referencing_symbols for a record."""
126 |         # Test referencing symbols for user record
127 |         file_path = os.path.join("include", "records.hrl")
128 | 
129 |         symbols = language_server.request_document_symbols(file_path)
130 |         user_symbol = None
131 |         for symbol_group in symbols:
132 |             user_symbol = next((s for s in symbol_group if "user" in s.get("name", "")), None)
133 |             if user_symbol:
134 |                 break
135 | 
136 |         if not user_symbol or "selectionRange" not in user_symbol:
137 |             pytest.skip("User record symbol or its selectionRange not found")
138 | 
139 |         sel_start = user_symbol["selectionRange"]["start"]
140 |         ref_symbols = [
141 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
142 |         ]
143 | 
144 |         if ref_symbols:
145 |             models_references = [
146 |                 symbol
147 |                 for symbol in ref_symbols
148 |                 if "location" in symbol and "uri" in symbol["location"] and "models.erl" in symbol["location"]["uri"]
149 |             ]
150 |             # We expect some references from models.erl
151 |             assert len(models_references) >= 0  # At least attempt to find references
152 | 
153 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
154 |     def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None:
155 |         """Test request_referencing_symbols for a function."""
156 |         # Test referencing symbols for create_user function
157 |         file_path = os.path.join("src", "models.erl")
158 | 
159 |         symbols = language_server.request_document_symbols(file_path)
160 |         create_user_symbol = None
161 |         for symbol_group in symbols:
162 |             create_user_symbol = next((s for s in symbol_group if "create_user" in s.get("name", "")), None)
163 |             if create_user_symbol:
164 |                 break
165 | 
166 |         if not create_user_symbol or "selectionRange" not in create_user_symbol:
167 |             pytest.skip("create_user function symbol or its selectionRange not found")
168 | 
169 |         sel_start = create_user_symbol["selectionRange"]["start"]
170 |         ref_symbols = [
171 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
172 |         ]
173 | 
174 |         if ref_symbols:
175 |             # We might find references from services.erl or test files
176 |             service_references = [
177 |                 symbol
178 |                 for symbol in ref_symbols
179 |                 if "location" in symbol
180 |                 and "uri" in symbol["location"]
181 |                 and ("services.erl" in symbol["location"]["uri"] or "test" in symbol["location"]["uri"])
182 |             ]
183 |             assert len(service_references) >= 0  # At least attempt to find references
184 | 
185 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
186 |     def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
187 |         """Test request_referencing_symbols for a position with no symbol."""
188 |         file_path = os.path.join("src", "models.erl")
189 |         # Line 3 is likely a blank line or comment
190 |         try:
191 |             ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 3, 0)]
192 |             # If we get here, make sure we got an empty result
193 |             assert ref_symbols == [] or ref_symbols is None
194 |         except Exception:
195 |             # The method might raise an exception for invalid positions
196 |             # which is acceptable behavior
197 |             pass
198 | 
199 |     # Tests for request_defining_symbol
200 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
201 |     def test_request_defining_symbol_function_call(self, language_server: SolidLanguageServer) -> None:
202 |         """Test request_defining_symbol for a function call."""
203 |         # Find a place where models:create_user is called in services.erl
204 |         file_path = os.path.join("src", "services.erl")
205 |         content = language_server.retrieve_full_file_content(file_path)
206 |         lines = content.split("\n")
207 |         models_call_line = None
208 |         for i, line in enumerate(lines):
209 |             if "models:create_user(" in line:
210 |                 models_call_line = i
211 |                 break
212 | 
213 |         if models_call_line is None:
214 |             pytest.skip("Could not find models:create_user call")
215 | 
216 |         # Try to find the definition of models:create_user
217 |         defining_symbol = language_server.request_defining_symbol(file_path, models_call_line, 20)
218 | 
219 |         if defining_symbol:
220 |             assert "create_user" in defining_symbol.get("name", "") or "models" in defining_symbol.get("name", "")
221 |             if "location" in defining_symbol and "uri" in defining_symbol["location"]:
222 |                 assert "models.erl" in defining_symbol["location"]["uri"]
223 | 
224 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
225 |     def test_request_defining_symbol_record_usage(self, language_server: SolidLanguageServer) -> None:
226 |         """Test request_defining_symbol for a record usage."""
227 |         # Find a place where #user{} record is used in models.erl
228 |         file_path = os.path.join("src", "models.erl")
229 |         content = language_server.retrieve_full_file_content(file_path)
230 |         lines = content.split("\n")
231 |         record_usage_line = None
232 |         for i, line in enumerate(lines):
233 |             if "#user{" in line:
234 |                 record_usage_line = i
235 |                 break
236 | 
237 |         if record_usage_line is None:
238 |             pytest.skip("Could not find #user{} record usage")
239 | 
240 |         defining_symbol = language_server.request_defining_symbol(file_path, record_usage_line, 10)
241 | 
242 |         if defining_symbol:
243 |             assert "user" in defining_symbol.get("name", "").lower()
244 |             if "location" in defining_symbol and "uri" in defining_symbol["location"]:
245 |                 assert "records.hrl" in defining_symbol["location"]["uri"]
246 | 
247 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
248 |     def test_request_defining_symbol_module_call(self, language_server: SolidLanguageServer) -> None:
249 |         """Test request_defining_symbol for a module function call."""
250 |         # Find a place where utils:validate_input is called
251 |         file_path = os.path.join("src", "models.erl")
252 |         content = language_server.retrieve_full_file_content(file_path)
253 |         lines = content.split("\n")
254 |         utils_call_line = None
255 |         for i, line in enumerate(lines):
256 |             if "validate_email(" in line:
257 |                 utils_call_line = i
258 |                 break
259 | 
260 |         if utils_call_line is None:
261 |             pytest.skip("Could not find function call in models.erl")
262 | 
263 |         defining_symbol = language_server.request_defining_symbol(file_path, utils_call_line, 15)
264 | 
265 |         if defining_symbol:
266 |             assert "validate" in defining_symbol.get("name", "") or "email" in defining_symbol.get("name", "")
267 | 
268 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
269 |     def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
270 |         """Test request_defining_symbol for a position with no symbol."""
271 |         # Test for a position with no symbol (e.g., whitespace or comment)
272 |         file_path = os.path.join("src", "models.erl")
273 |         # Line 3 is likely a blank line or comment
274 |         defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)
275 | 
276 |         # Should return None or empty
277 |         assert defining_symbol is None or defining_symbol == {}
278 | 
279 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
280 |     def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
281 |         """Test integration between different symbol methods."""
282 |         file_path = os.path.join("src", "models.erl")
283 | 
284 |         # Find create_user function definition
285 |         content = language_server.retrieve_full_file_content(file_path)
286 |         lines = content.split("\n")
287 |         create_user_line = None
288 |         for i, line in enumerate(lines):
289 |             if "create_user(" in line and "-spec" not in line:
290 |                 create_user_line = i
291 |                 break
292 | 
293 |         if create_user_line is None:
294 |             pytest.skip("Could not find create_user function")
295 | 
296 |         # Test containing symbol
297 |         containing = language_server.request_containing_symbol(file_path, create_user_line + 2, 10)
298 | 
299 |         if containing:
300 |             # Test that we can find references to this symbol
301 |             if "location" in containing and "range" in containing["location"]:
302 |                 start_pos = containing["location"]["range"]["start"]
303 |                 refs = [
304 |                     ref.symbol for ref in language_server.request_referencing_symbols(file_path, start_pos["line"], start_pos["character"])
305 |                 ]
306 |                 # We should find some references or none (both are valid outcomes)
307 |                 assert isinstance(refs, list)
308 | 
309 |     @pytest.mark.timeout(120)  # Add explicit timeout for this complex test
310 |     @pytest.mark.xfail(
311 |         reason="Known intermittent timeout issue in Erlang LS in CI environments. "
312 |         "May pass locally but can timeout on slower CI systems.",
313 |         strict=False,
314 |     )
315 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
316 |     def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
317 |         """Test that symbol tree structure is correctly built."""
318 |         symbol_tree = language_server.request_full_symbol_tree()
319 | 
320 |         # Should get a tree structure
321 |         assert len(symbol_tree) > 0
322 | 
323 |         # Should have our test repository structure
324 |         root = symbol_tree[0]
325 |         assert "children" in root
326 | 
327 |         # Look for src directory
328 |         src_dir = None
329 |         for child in root["children"]:
330 |             if child["name"] == "src":
331 |                 src_dir = child
332 |                 break
333 | 
334 |         if src_dir:
335 |             # Check for our Erlang modules
336 |             file_names = [child["name"] for child in src_dir.get("children", [])]
337 |             expected_modules = ["models", "services", "utils", "app"]
338 |             found_modules = [name for name in expected_modules if any(name in fname for fname in file_names)]
339 |             assert len(found_modules) > 0, f"Expected to find some modules from {expected_modules}, but got {file_names}"
340 | 
341 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
342 |     def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
343 |         """Test request_dir_overview functionality."""
344 |         src_overview = language_server.request_dir_overview("src")
345 | 
346 |         # Should get an overview of the src directory
347 |         assert src_overview is not None
348 |         overview_keys = list(src_overview.keys()) if hasattr(src_overview, "keys") else []
349 |         src_files = [key for key in overview_keys if key.startswith("src/") or "src" in key]
350 |         assert len(src_files) > 0, f"Expected to find src/ files in overview keys: {overview_keys}"
351 | 
352 |         # Should contain information about our modules
353 |         overview_text = str(src_overview).lower()
354 |         expected_terms = ["models", "services", "user", "create_user", "gen_server"]
355 |         found_terms = [term for term in expected_terms if term in overview_text]
356 |         assert len(found_terms) > 0, f"Expected to find some terms from {expected_terms} in overview"
357 | 
358 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
359 |     def test_containing_symbol_of_record_field(self, language_server: SolidLanguageServer) -> None:
360 |         """Test containing symbol for record field access."""
361 |         file_path = os.path.join("src", "models.erl")
362 | 
363 |         # Find a record field access like User#user.name
364 |         content = language_server.retrieve_full_file_content(file_path)
365 |         lines = content.split("\n")
366 |         record_field_line = None
367 |         for i, line in enumerate(lines):
368 |             if "#user{" in line and ("name" in line or "email" in line or "id" in line):
369 |                 record_field_line = i
370 |                 break
371 | 
372 |         if record_field_line is None:
373 |             pytest.skip("Could not find record field access")
374 | 
375 |         containing_symbol = language_server.request_containing_symbol(file_path, record_field_line, 10)
376 | 
377 |         if containing_symbol:
378 |             # Should be contained within a function
379 |             assert "name" in containing_symbol
380 |             expected_names = ["create_user", "update_user", "format_user_info"]
381 |             assert any(name in containing_symbol["name"] for name in expected_names)
382 | 
383 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
384 |     def test_containing_symbol_of_spec(self, language_server: SolidLanguageServer) -> None:
385 |         """Test containing symbol for function specs."""
386 |         file_path = os.path.join("src", "models.erl")
387 | 
388 |         # Find a -spec directive
389 |         content = language_server.retrieve_full_file_content(file_path)
390 |         lines = content.split("\n")
391 |         spec_line = None
392 |         for i, line in enumerate(lines):
393 |             if line.strip().startswith("-spec") and "create_user" in line:
394 |                 spec_line = i
395 |                 break
396 | 
397 |         if spec_line is None:
398 |             pytest.skip("Could not find -spec directive")
399 | 
400 |         containing_symbol = language_server.request_containing_symbol(file_path, spec_line, 5)
401 | 
402 |         if containing_symbol:
403 |             # Should be contained within the module or the function it specifies
404 |             assert "name" in containing_symbol
405 |             expected_names = ["models", "create_user"]
406 |             assert any(name in containing_symbol["name"] for name in expected_names)
407 | 
408 |     @pytest.mark.timeout(90)  # Add explicit timeout
409 |     @pytest.mark.xfail(
410 |         reason="Known intermittent timeout issue in Erlang LS in CI environments. "
411 |         "May pass locally but can timeout on slower CI systems, especially macOS. "
412 |         "Similar to known Next LS timeout issues.",
413 |         strict=False,
414 |     )
415 |     @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
416 |     def test_referencing_symbols_across_files(self, language_server: SolidLanguageServer) -> None:
417 |         """Test finding references across different files."""
418 |         # Test that we can find references to models module functions in services.erl
419 |         file_path = os.path.join("src", "models.erl")
420 | 
421 |         symbols = language_server.request_document_symbols(file_path)
422 |         create_user_symbol = None
423 |         for symbol_group in symbols:
424 |             create_user_symbol = next((s for s in symbol_group if "create_user" in s.get("name", "")), None)
425 |             if create_user_symbol:
426 |                 break
427 | 
428 |         if not create_user_symbol or "selectionRange" not in create_user_symbol:
429 |             pytest.skip("create_user function symbol not found")
430 | 
431 |         sel_start = create_user_symbol["selectionRange"]["start"]
432 |         ref_symbols = [
433 |             ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
434 |         ]
435 | 
436 |         # Look for cross-file references
437 |         cross_file_refs = [
438 |             symbol
439 |             for symbol in ref_symbols
440 |             if "location" in symbol and "uri" in symbol["location"] and not symbol["location"]["uri"].endswith("models.erl")
441 |         ]
442 | 
443 |         # We might find references in services.erl or test files
444 |         if cross_file_refs:
445 |             assert len(cross_file_refs) > 0, "Should find some cross-file references"
446 | 
```

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

```python
  1 | """
  2 | Ruby LSP Language Server implementation using Shopify's ruby-lsp.
  3 | Provides modern Ruby language server capabilities with improved performance.
  4 | """
  5 | 
  6 | import json
  7 | import logging
  8 | import os
  9 | import pathlib
 10 | import shutil
 11 | import subprocess
 12 | import threading
 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.lsp_protocol_handler.lsp_types import InitializeParams
 20 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 21 | from solidlsp.settings import SolidLSPSettings
 22 | 
 23 | 
 24 | class RubyLsp(SolidLanguageServer):
 25 |     """
 26 |     Provides Ruby specific instantiation of the LanguageServer class using ruby-lsp.
 27 |     Contains various configurations and settings specific to Ruby with modern LSP features.
 28 |     """
 29 | 
 30 |     def __init__(
 31 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 32 |     ):
 33 |         """
 34 |         Creates a RubyLsp instance. This class is not meant to be instantiated directly.
 35 |         Use LanguageServer.create() instead.
 36 |         """
 37 |         ruby_lsp_executable = self._setup_runtime_dependencies(logger, config, repository_root_path)
 38 |         super().__init__(
 39 |             config,
 40 |             logger,
 41 |             repository_root_path,
 42 |             ProcessLaunchInfo(cmd=ruby_lsp_executable, cwd=repository_root_path),
 43 |             "ruby",
 44 |             solidlsp_settings,
 45 |         )
 46 |         self.analysis_complete = threading.Event()
 47 |         self.service_ready_event = threading.Event()
 48 | 
 49 |         # Set timeout for ruby-lsp requests - ruby-lsp is fast
 50 |         self.set_request_timeout(30.0)  # 30 seconds for initialization and requests
 51 | 
 52 |     @override
 53 |     def is_ignored_dirname(self, dirname: str) -> bool:
 54 |         """Override to ignore Ruby-specific directories that cause performance issues."""
 55 |         ruby_ignored_dirs = [
 56 |             "vendor",  # Ruby vendor directory
 57 |             ".bundle",  # Bundler cache
 58 |             "tmp",  # Temporary files
 59 |             "log",  # Log files
 60 |             "coverage",  # Test coverage reports
 61 |             ".yardoc",  # YARD documentation cache
 62 |             "doc",  # Generated documentation
 63 |             "node_modules",  # Node modules (for Rails with JS)
 64 |             "storage",  # Active Storage files (Rails)
 65 |             "public/packs",  # Webpacker output
 66 |             "public/webpack",  # Webpack output
 67 |             "public/assets",  # Rails compiled assets
 68 |         ]
 69 |         return super().is_ignored_dirname(dirname) or dirname in ruby_ignored_dirs
 70 | 
 71 |     @override
 72 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
 73 |         """Override to provide optimal wait time for ruby-lsp cross-file reference resolution.
 74 | 
 75 |         ruby-lsp typically initializes quickly, but may need a brief moment
 76 |         for cross-file analysis in larger projects.
 77 |         """
 78 |         return 0.5  # 500ms should be sufficient for ruby-lsp
 79 | 
 80 |     @staticmethod
 81 |     def _find_executable_with_extensions(executable_name: str) -> str | None:
 82 |         """
 83 |         Find executable with Windows-specific extensions (.bat, .cmd, .exe) if on Windows.
 84 |         Returns the full path to the executable or None if not found.
 85 |         """
 86 |         import platform
 87 | 
 88 |         if platform.system() == "Windows":
 89 |             # Try Windows-specific extensions first
 90 |             for ext in [".bat", ".cmd", ".exe"]:
 91 |                 path = shutil.which(f"{executable_name}{ext}")
 92 |                 if path:
 93 |                     return path
 94 |             # Fall back to default search
 95 |             return shutil.which(executable_name)
 96 |         else:
 97 |             # Unix systems
 98 |             return shutil.which(executable_name)
 99 | 
100 |     @staticmethod
101 |     def _setup_runtime_dependencies(logger: LanguageServerLogger, config: LanguageServerConfig, repository_root_path: str) -> list[str]:
102 |         """
103 |         Setup runtime dependencies for ruby-lsp and return the command list to start the server.
104 |         Installation strategy: Bundler project > global ruby-lsp > gem install ruby-lsp
105 |         """
106 |         # Detect rbenv-managed Ruby environment
107 |         # When .ruby-version exists, it indicates the project uses rbenv for version management.
108 |         # rbenv automatically reads .ruby-version to determine which Ruby version to use.
109 |         # Using "rbenv exec" ensures commands run with the correct Ruby version and its gems.
110 |         #
111 |         # Why rbenv is preferred over system Ruby:
112 |         # - Respects project-specific Ruby versions
113 |         # - Avoids bundler version mismatches between system and project
114 |         # - Ensures consistent environment across developers
115 |         #
116 |         # Fallback behavior:
117 |         # If .ruby-version doesn't exist or rbenv isn't installed, we fall back to system Ruby.
118 |         # This may cause issues if:
119 |         # - System Ruby version differs from what the project expects
120 |         # - System bundler version is incompatible with Gemfile.lock
121 |         # - Project gems aren't installed in system Ruby
122 |         ruby_version_file = os.path.join(repository_root_path, ".ruby-version")
123 |         use_rbenv = os.path.exists(ruby_version_file) and shutil.which("rbenv") is not None
124 | 
125 |         if use_rbenv:
126 |             ruby_cmd = ["rbenv", "exec", "ruby"]
127 |             bundle_cmd = ["rbenv", "exec", "bundle"]
128 |             logger.log(f"Using rbenv-managed Ruby (found {ruby_version_file})", logging.INFO)
129 |         else:
130 |             ruby_cmd = ["ruby"]
131 |             bundle_cmd = ["bundle"]
132 |             if os.path.exists(ruby_version_file):
133 |                 logger.log(
134 |                     f"Found {ruby_version_file} but rbenv is not installed. "
135 |                     "Using system Ruby. Consider installing rbenv for better version management: https://github.com/rbenv/rbenv",
136 |                     logging.WARNING,
137 |                 )
138 |             else:
139 |                 logger.log("No .ruby-version file found, using system Ruby", logging.INFO)
140 | 
141 |         # Check if Ruby is installed
142 |         try:
143 |             result = subprocess.run(ruby_cmd + ["--version"], check=True, capture_output=True, cwd=repository_root_path, text=True)
144 |             ruby_version = result.stdout.strip()
145 |             logger.log(f"Ruby version: {ruby_version}", logging.INFO)
146 | 
147 |             # Extract version number for compatibility checks
148 |             import re
149 | 
150 |             version_match = re.search(r"ruby (\d+)\.(\d+)\.(\d+)", ruby_version)
151 |             if version_match:
152 |                 major, minor, patch = map(int, version_match.groups())
153 |                 if major < 2 or (major == 2 and minor < 6):
154 |                     logger.log(f"Warning: Ruby {major}.{minor}.{patch} detected. ruby-lsp works best with Ruby 2.6+", logging.WARNING)
155 | 
156 |         except subprocess.CalledProcessError as e:
157 |             error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode() if e.stderr else "Unknown error"
158 |             raise RuntimeError(
159 |                 f"Error checking Ruby installation: {error_msg}. Please ensure Ruby is properly installed and in PATH."
160 |             ) from e
161 |         except FileNotFoundError as e:
162 |             raise RuntimeError(
163 |                 "Ruby is not installed or not found in PATH. Please install Ruby using one of these methods:\n"
164 |                 "  - Using rbenv: rbenv install 3.0.0 && rbenv global 3.0.0\n"
165 |                 "  - Using RVM: rvm install 3.0.0 && rvm use 3.0.0 --default\n"
166 |                 "  - Using asdf: asdf install ruby 3.0.0 && asdf global ruby 3.0.0\n"
167 |                 "  - System package manager (brew install ruby, apt install ruby, etc.)"
168 |             ) from e
169 | 
170 |         # Check for Bundler project (Gemfile exists)
171 |         gemfile_path = os.path.join(repository_root_path, "Gemfile")
172 |         gemfile_lock_path = os.path.join(repository_root_path, "Gemfile.lock")
173 |         is_bundler_project = os.path.exists(gemfile_path)
174 | 
175 |         if is_bundler_project:
176 |             logger.log("Detected Bundler project (Gemfile found)", logging.INFO)
177 | 
178 |             # Check if bundle command is available using Windows-compatible search
179 |             bundle_path = RubyLsp._find_executable_with_extensions(bundle_cmd[0] if len(bundle_cmd) == 1 else "bundle")
180 |             if not bundle_path:
181 |                 # Try common bundle executables
182 |                 for bundle_executable in ["bin/bundle", "bundle"]:
183 |                     if bundle_executable.startswith("bin/"):
184 |                         bundle_full_path = os.path.join(repository_root_path, bundle_executable)
185 |                     else:
186 |                         bundle_full_path = RubyLsp._find_executable_with_extensions(bundle_executable)
187 |                     if bundle_full_path and os.path.exists(bundle_full_path):
188 |                         bundle_path = bundle_full_path if bundle_executable.startswith("bin/") else bundle_executable
189 |                         break
190 | 
191 |             if not bundle_path:
192 |                 logger.log(
193 |                     "Bundler project detected but 'bundle' command not found. Falling back to global ruby-lsp installation.",
194 |                     logging.WARNING,
195 |                 )
196 |             else:
197 |                 # Check if ruby-lsp is in Gemfile.lock
198 |                 ruby_lsp_in_bundle = False
199 |                 if os.path.exists(gemfile_lock_path):
200 |                     try:
201 |                         with open(gemfile_lock_path) as f:
202 |                             content = f.read()
203 |                             ruby_lsp_in_bundle = "ruby-lsp" in content.lower()
204 |                     except Exception as e:
205 |                         logger.log(f"Warning: Could not read Gemfile.lock: {e}", logging.WARNING)
206 | 
207 |                 if ruby_lsp_in_bundle:
208 |                     logger.log("Found ruby-lsp in Gemfile.lock", logging.INFO)
209 |                     return bundle_cmd + ["exec", "ruby-lsp"]
210 |                 else:
211 |                     logger.log(
212 |                         "ruby-lsp not found in Gemfile.lock. Consider adding 'gem \"ruby-lsp\"' to your Gemfile for better compatibility.",
213 |                         logging.INFO,
214 |                     )
215 |                     # Fall through to global installation check
216 | 
217 |         # Check if ruby-lsp is available globally using Windows-compatible search
218 |         ruby_lsp_path = RubyLsp._find_executable_with_extensions("ruby-lsp")
219 |         if ruby_lsp_path:
220 |             logger.log(f"Found ruby-lsp at: {ruby_lsp_path}", logging.INFO)
221 |             return [ruby_lsp_path]
222 | 
223 |         # Try to install ruby-lsp globally
224 |         logger.log("ruby-lsp not found, attempting to install globally...", logging.INFO)
225 |         try:
226 |             subprocess.run(["gem", "install", "ruby-lsp"], check=True, capture_output=True, cwd=repository_root_path)
227 |             logger.log("Successfully installed ruby-lsp globally", logging.INFO)
228 |             # Find the newly installed ruby-lsp executable
229 |             ruby_lsp_path = RubyLsp._find_executable_with_extensions("ruby-lsp")
230 |             return [ruby_lsp_path] if ruby_lsp_path else ["ruby-lsp"]
231 |         except subprocess.CalledProcessError as e:
232 |             error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode() if e.stderr else str(e)
233 |             if is_bundler_project:
234 |                 raise RuntimeError(
235 |                     f"Failed to install ruby-lsp globally: {error_msg}\n"
236 |                     "For Bundler projects, please add 'gem \"ruby-lsp\"' to your Gemfile and run 'bundle install'.\n"
237 |                     "Alternatively, install globally: gem install ruby-lsp"
238 |                 ) from e
239 |             raise RuntimeError(f"Failed to install ruby-lsp: {error_msg}\nPlease try installing manually: gem install ruby-lsp") from e
240 | 
241 |     @staticmethod
242 |     def _detect_rails_project(repository_root_path: str) -> bool:
243 |         """
244 |         Detect if this is a Rails project by checking for Rails-specific files.
245 |         """
246 |         rails_indicators = [
247 |             "config/application.rb",
248 |             "config/environment.rb",
249 |             "app/controllers/application_controller.rb",
250 |             "Rakefile",
251 |         ]
252 | 
253 |         for indicator in rails_indicators:
254 |             if os.path.exists(os.path.join(repository_root_path, indicator)):
255 |                 return True
256 | 
257 |         # Check for Rails in Gemfile
258 |         gemfile_path = os.path.join(repository_root_path, "Gemfile")
259 |         if os.path.exists(gemfile_path):
260 |             try:
261 |                 with open(gemfile_path) as f:
262 |                     content = f.read().lower()
263 |                     if "gem 'rails'" in content or 'gem "rails"' in content:
264 |                         return True
265 |             except Exception:
266 |                 pass
267 | 
268 |         return False
269 | 
270 |     @staticmethod
271 |     def _get_ruby_exclude_patterns(repository_root_path: str) -> list[str]:
272 |         """
273 |         Get Ruby and Rails-specific exclude patterns for better performance.
274 |         """
275 |         base_patterns = [
276 |             "**/vendor/**",  # Ruby vendor directory
277 |             "**/.bundle/**",  # Bundler cache
278 |             "**/tmp/**",  # Temporary files
279 |             "**/log/**",  # Log files
280 |             "**/coverage/**",  # Test coverage reports
281 |             "**/.yardoc/**",  # YARD documentation cache
282 |             "**/doc/**",  # Generated documentation
283 |             "**/.git/**",  # Git directory
284 |             "**/node_modules/**",  # Node modules (for Rails with JS)
285 |             "**/public/assets/**",  # Rails compiled assets
286 |         ]
287 | 
288 |         # Add Rails-specific patterns if this is a Rails project
289 |         if RubyLsp._detect_rails_project(repository_root_path):
290 |             base_patterns.extend(
291 |                 [
292 |                     "**/app/assets/builds/**",  # Rails 7+ CSS builds
293 |                     "**/storage/**",  # Active Storage
294 |                     "**/public/packs/**",  # Webpacker
295 |                     "**/public/webpack/**",  # Webpack
296 |                 ]
297 |             )
298 | 
299 |         return base_patterns
300 | 
301 |     def _get_initialize_params(self) -> InitializeParams:
302 |         """
303 |         Returns ruby-lsp specific initialization parameters.
304 |         """
305 |         exclude_patterns = self._get_ruby_exclude_patterns(self.repository_root_path)
306 | 
307 |         initialize_params = {
308 |             "processId": os.getpid(),
309 |             "rootPath": self.repository_root_path,
310 |             "rootUri": pathlib.Path(self.repository_root_path).as_uri(),
311 |             "capabilities": {
312 |                 "workspace": {
313 |                     "workspaceEdit": {"documentChanges": True},
314 |                     "configuration": True,
315 |                 },
316 |                 "window": {
317 |                     "workDoneProgress": True,
318 |                 },
319 |                 "textDocument": {
320 |                     "documentSymbol": {
321 |                         "hierarchicalDocumentSymbolSupport": True,
322 |                         "symbolKind": {"valueSet": list(range(1, 27))},
323 |                     },
324 |                     "completion": {
325 |                         "completionItem": {
326 |                             "snippetSupport": True,
327 |                             "commitCharactersSupport": True,
328 |                         }
329 |                     },
330 |                 },
331 |             },
332 |             "initializationOptions": {
333 |                 # ruby-lsp enables all features by default, so we don't need to specify enabledFeatures
334 |                 "experimentalFeaturesEnabled": False,
335 |                 "featuresConfiguration": {},
336 |                 "indexing": {
337 |                     "includedPatterns": ["**/*.rb", "**/*.rake", "**/*.ru", "**/*.erb"],
338 |                     "excludedPatterns": exclude_patterns,
339 |                 },
340 |             },
341 |         }
342 | 
343 |         return initialize_params
344 | 
345 |     def _start_server(self) -> None:
346 |         """
347 |         Starts the ruby-lsp Language Server for Ruby
348 |         """
349 | 
350 |         def register_capability_handler(params: dict) -> None:
351 |             assert "registrations" in params
352 |             for registration in params["registrations"]:
353 |                 self.logger.log(f"Registered capability: {registration['method']}", logging.INFO)
354 |             return
355 | 
356 |         def lang_status_handler(params: dict) -> None:
357 |             self.logger.log(f"LSP: language/status: {params}", logging.INFO)
358 |             if params.get("type") == "ready":
359 |                 self.logger.log("ruby-lsp service is ready.", logging.INFO)
360 |                 self.analysis_complete.set()
361 |                 self.completions_available.set()
362 | 
363 |         def execute_client_command_handler(params: dict) -> list:
364 |             return []
365 | 
366 |         def do_nothing(params: dict) -> None:
367 |             return
368 | 
369 |         def window_log_message(msg: dict) -> None:
370 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
371 | 
372 |         def progress_handler(params: dict) -> None:
373 |             # ruby-lsp sends progress notifications during indexing
374 |             self.logger.log(f"LSP: $/progress: {params}", logging.DEBUG)
375 |             if "value" in params:
376 |                 value = params["value"]
377 |                 # Check for completion indicators
378 |                 if value.get("kind") == "end":
379 |                     self.logger.log("ruby-lsp indexing complete ($/progress end)", logging.INFO)
380 |                     self.analysis_complete.set()
381 |                     self.completions_available.set()
382 |                 elif value.get("kind") == "begin":
383 |                     self.logger.log("ruby-lsp indexing started ($/progress begin)", logging.INFO)
384 |                 elif "percentage" in value:
385 |                     percentage = value.get("percentage", 0)
386 |                     self.logger.log(f"ruby-lsp indexing progress: {percentage}%", logging.DEBUG)
387 |             # Handle direct progress format (fallback)
388 |             elif "token" in params and "value" in params:
389 |                 token = params.get("token")
390 |                 if isinstance(token, str) and "indexing" in token.lower():
391 |                     value = params.get("value", {})
392 |                     if value.get("kind") == "end" or value.get("percentage") == 100:
393 |                         self.logger.log("ruby-lsp indexing complete (token progress)", logging.INFO)
394 |                         self.analysis_complete.set()
395 |                         self.completions_available.set()
396 | 
397 |         def window_work_done_progress_create(params: dict) -> None:
398 |             """Handle workDoneProgress/create requests from ruby-lsp"""
399 |             self.logger.log(f"LSP: window/workDoneProgress/create: {params}", logging.DEBUG)
400 |             return {}
401 | 
402 |         self.server.on_request("client/registerCapability", register_capability_handler)
403 |         self.server.on_notification("language/status", lang_status_handler)
404 |         self.server.on_notification("window/logMessage", window_log_message)
405 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
406 |         self.server.on_notification("$/progress", progress_handler)
407 |         self.server.on_request("window/workDoneProgress/create", window_work_done_progress_create)
408 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
409 | 
410 |         self.logger.log("Starting ruby-lsp server process", logging.INFO)
411 |         self.server.start()
412 |         initialize_params = self._get_initialize_params()
413 | 
414 |         self.logger.log(
415 |             "Sending initialize request from LSP client to LSP server and awaiting response",
416 |             logging.INFO,
417 |         )
418 |         self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO)
419 |         init_response = self.server.send.initialize(initialize_params)
420 |         self.logger.log(f"Received init response: {init_response}", logging.INFO)
421 | 
422 |         # Verify expected capabilities
423 |         # Note: ruby-lsp may return textDocumentSync in different formats (number or object)
424 |         text_document_sync = init_response["capabilities"].get("textDocumentSync")
425 |         if isinstance(text_document_sync, int):
426 |             assert text_document_sync in [1, 2], f"Unexpected textDocumentSync value: {text_document_sync}"
427 |         elif isinstance(text_document_sync, dict):
428 |             # ruby-lsp returns an object with change property
429 |             assert "change" in text_document_sync, "textDocumentSync object should have 'change' property"
430 | 
431 |         assert "completionProvider" in init_response["capabilities"]
432 | 
433 |         self.server.notify.initialized({})
434 |         # Wait for ruby-lsp to complete its initial indexing
435 |         # ruby-lsp has fast indexing
436 |         self.logger.log("Waiting for ruby-lsp to complete initial indexing...", logging.INFO)
437 |         if self.analysis_complete.wait(timeout=30.0):
438 |             self.logger.log("ruby-lsp initial indexing complete, server ready", logging.INFO)
439 |         else:
440 |             self.logger.log("Timeout waiting for ruby-lsp indexing completion, proceeding anyway", logging.WARNING)
441 |             # Fallback: assume indexing is complete after timeout
442 |             self.analysis_complete.set()
443 |             self.completions_available.set()
444 | 
445 |     def _handle_initialization_response(self, init_response):
446 |         """
447 |         Handle the initialization response from ruby-lsp and validate capabilities.
448 |         """
449 |         if "capabilities" in init_response:
450 |             capabilities = init_response["capabilities"]
451 | 
452 |             # Validate textDocumentSync (ruby-lsp may return different formats)
453 |             text_document_sync = capabilities.get("textDocumentSync")
454 |             if isinstance(text_document_sync, int):
455 |                 assert text_document_sync in [1, 2], f"Unexpected textDocumentSync value: {text_document_sync}"
456 |             elif isinstance(text_document_sync, dict):
457 |                 # ruby-lsp returns an object with change property
458 |                 assert "change" in text_document_sync, "textDocumentSync object should have 'change' property"
459 | 
460 |             # Log important capabilities
461 |             important_capabilities = [
462 |                 "completionProvider",
463 |                 "hoverProvider",
464 |                 "definitionProvider",
465 |                 "referencesProvider",
466 |                 "documentSymbolProvider",
467 |                 "codeActionProvider",
468 |                 "documentFormattingProvider",
469 |                 "semanticTokensProvider",
470 |             ]
471 | 
472 |             for cap in important_capabilities:
473 |                 if cap in capabilities:
474 |                     self.logger.log(f"ruby-lsp {cap}: available", logging.DEBUG)
475 | 
476 |         # Signal that the service is ready
477 |         self.service_ready_event.set()
478 | 
```

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

```python
  1 | """
  2 | Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
  3 | """
  4 | 
  5 | import dataclasses
  6 | import logging
  7 | import os
  8 | import pathlib
  9 | import stat
 10 | 
 11 | from solidlsp.ls import SolidLanguageServer
 12 | from solidlsp.ls_config import LanguageServerConfig
 13 | from solidlsp.ls_logger import LanguageServerLogger
 14 | from solidlsp.ls_utils import FileUtils, PlatformUtils
 15 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 16 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 17 | from solidlsp.settings import SolidLSPSettings
 18 | 
 19 | 
 20 | @dataclasses.dataclass
 21 | class KotlinRuntimeDependencyPaths:
 22 |     """
 23 |     Stores the paths to the runtime dependencies of Kotlin Language Server
 24 |     """
 25 | 
 26 |     java_path: str
 27 |     java_home_path: str
 28 |     kotlin_executable_path: str
 29 | 
 30 | 
 31 | class KotlinLanguageServer(SolidLanguageServer):
 32 |     """
 33 |     Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin.
 34 |     """
 35 | 
 36 |     def __init__(
 37 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 38 |     ):
 39 |         """
 40 |         Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 41 |         """
 42 |         runtime_dependency_paths = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 43 |         self.runtime_dependency_paths = runtime_dependency_paths
 44 | 
 45 |         # Create command to execute the Kotlin Language Server script
 46 |         cmd = [self.runtime_dependency_paths.kotlin_executable_path, "--stdio"]
 47 | 
 48 |         # Set environment variables including JAVA_HOME
 49 |         proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path}
 50 | 
 51 |         super().__init__(
 52 |             config,
 53 |             logger,
 54 |             repository_root_path,
 55 |             ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path),
 56 |             "kotlin",
 57 |             solidlsp_settings,
 58 |         )
 59 | 
 60 |     @classmethod
 61 |     def _setup_runtime_dependencies(
 62 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
 63 |     ) -> KotlinRuntimeDependencyPaths:
 64 |         """
 65 |         Setup runtime dependencies for Kotlin Language Server and return the paths.
 66 |         """
 67 |         platform_id = PlatformUtils.get_platform_id()
 68 | 
 69 |         # Verify platform support
 70 |         assert (
 71 |             platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-")
 72 |         ), "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment"
 73 | 
 74 |         # Runtime dependency information
 75 |         runtime_dependencies = {
 76 |             "runtimeDependency": {
 77 |                 "id": "KotlinLsp",
 78 |                 "description": "Kotlin Language Server",
 79 |                 "url": "https://download-cdn.jetbrains.com/kotlin-lsp/0.253.10629/kotlin-0.253.10629.zip",
 80 |                 "archiveType": "zip",
 81 |             },
 82 |             "java": {
 83 |                 "win-x64": {
 84 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix",
 85 |                     "archiveType": "zip",
 86 |                     "java_home_path": "extension/jre/21.0.7-win32-x86_64",
 87 |                     "java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe",
 88 |                 },
 89 |                 "linux-x64": {
 90 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix",
 91 |                     "archiveType": "zip",
 92 |                     "java_home_path": "extension/jre/21.0.7-linux-x86_64",
 93 |                     "java_path": "extension/jre/21.0.7-linux-x86_64/bin/java",
 94 |                 },
 95 |                 "linux-arm64": {
 96 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix",
 97 |                     "archiveType": "zip",
 98 |                     "java_home_path": "extension/jre/21.0.7-linux-aarch64",
 99 |                     "java_path": "extension/jre/21.0.7-linux-aarch64/bin/java",
100 |                 },
101 |                 "osx-x64": {
102 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix",
103 |                     "archiveType": "zip",
104 |                     "java_home_path": "extension/jre/21.0.7-macosx-x86_64",
105 |                     "java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java",
106 |                 },
107 |                 "osx-arm64": {
108 |                     "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix",
109 |                     "archiveType": "zip",
110 |                     "java_home_path": "extension/jre/21.0.7-macosx-aarch64",
111 |                     "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java",
112 |                 },
113 |             },
114 |         }
115 | 
116 |         kotlin_dependency = runtime_dependencies["runtimeDependency"]
117 |         java_dependency = runtime_dependencies["java"][platform_id.value]
118 | 
119 |         # Setup paths for dependencies
120 |         static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "kotlin_language_server")
121 |         os.makedirs(static_dir, exist_ok=True)
122 | 
123 |         # Setup Java paths
124 |         java_dir = os.path.join(static_dir, "java")
125 |         os.makedirs(java_dir, exist_ok=True)
126 | 
127 |         java_home_path = os.path.join(java_dir, java_dependency["java_home_path"])
128 |         java_path = os.path.join(java_dir, java_dependency["java_path"])
129 | 
130 |         # Download and extract Java if not exists
131 |         if not os.path.exists(java_path):
132 |             logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO)
133 |             FileUtils.download_and_extract_archive(logger, java_dependency["url"], java_dir, java_dependency["archiveType"])
134 |             # Make Java executable
135 |             if not platform_id.value.startswith("win-"):
136 |                 os.chmod(java_path, 0o755)
137 | 
138 |         assert os.path.exists(java_path), f"Java executable not found at {java_path}"
139 | 
140 |         # Setup Kotlin Language Server paths
141 |         kotlin_ls_dir = static_dir
142 | 
143 |         # Get platform-specific executable script path
144 |         if platform_id.value.startswith("win-"):
145 |             kotlin_script = os.path.join(kotlin_ls_dir, "kotlin-lsp.cmd")
146 |         else:
147 |             kotlin_script = os.path.join(kotlin_ls_dir, "kotlin-lsp.sh")
148 | 
149 |         # Download and extract Kotlin Language Server if script doesn't exist
150 |         if not os.path.exists(kotlin_script):
151 |             logger.log("Downloading Kotlin Language Server...", logging.INFO)
152 |             FileUtils.download_and_extract_archive(logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"])
153 | 
154 |             # Make script executable on Unix platforms
155 |             if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"):
156 |                 os.chmod(
157 |                     kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH
158 |                 )
159 | 
160 |         # Use script file
161 |         if os.path.exists(kotlin_script):
162 |             kotlin_executable_path = kotlin_script
163 |             logger.log(f"Using Kotlin Language Server script at {kotlin_script}", logging.INFO)
164 |         else:
165 |             raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}")
166 | 
167 |         return KotlinRuntimeDependencyPaths(
168 |             java_path=java_path, java_home_path=java_home_path, kotlin_executable_path=kotlin_executable_path
169 |         )
170 | 
171 |     @staticmethod
172 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
173 |         """
174 |         Returns the initialize params for the Kotlin Language Server.
175 |         """
176 |         if not os.path.isabs(repository_absolute_path):
177 |             repository_absolute_path = os.path.abspath(repository_absolute_path)
178 | 
179 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
180 |         initialize_params = {
181 |             "clientInfo": {"name": "Multilspy Kotlin Client", "version": "1.0.0"},
182 |             "locale": "en",
183 |             "rootPath": repository_absolute_path,
184 |             "rootUri": root_uri,
185 |             "capabilities": {
186 |                 "workspace": {
187 |                     "applyEdit": True,
188 |                     "workspaceEdit": {
189 |                         "documentChanges": True,
190 |                         "resourceOperations": ["create", "rename", "delete"],
191 |                         "failureHandling": "textOnlyTransactional",
192 |                         "normalizesLineEndings": True,
193 |                         "changeAnnotationSupport": {"groupsOnLabel": True},
194 |                     },
195 |                     "didChangeConfiguration": {"dynamicRegistration": True},
196 |                     "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
197 |                     "symbol": {
198 |                         "dynamicRegistration": True,
199 |                         "symbolKind": {"valueSet": list(range(1, 27))},
200 |                         "tagSupport": {"valueSet": [1]},
201 |                         "resolveSupport": {"properties": ["location.range"]},
202 |                     },
203 |                     "codeLens": {"refreshSupport": True},
204 |                     "executeCommand": {"dynamicRegistration": True},
205 |                     "configuration": True,
206 |                     "workspaceFolders": True,
207 |                     "semanticTokens": {"refreshSupport": True},
208 |                     "fileOperations": {
209 |                         "dynamicRegistration": True,
210 |                         "didCreate": True,
211 |                         "didRename": True,
212 |                         "didDelete": True,
213 |                         "willCreate": True,
214 |                         "willRename": True,
215 |                         "willDelete": True,
216 |                     },
217 |                     "inlineValue": {"refreshSupport": True},
218 |                     "inlayHint": {"refreshSupport": True},
219 |                     "diagnostics": {"refreshSupport": True},
220 |                 },
221 |                 "textDocument": {
222 |                     "publishDiagnostics": {
223 |                         "relatedInformation": True,
224 |                         "versionSupport": False,
225 |                         "tagSupport": {"valueSet": [1, 2]},
226 |                         "codeDescriptionSupport": True,
227 |                         "dataSupport": True,
228 |                     },
229 |                     "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True},
230 |                     "completion": {
231 |                         "dynamicRegistration": True,
232 |                         "contextSupport": True,
233 |                         "completionItem": {
234 |                             "snippetSupport": False,
235 |                             "commitCharactersSupport": True,
236 |                             "documentationFormat": ["markdown", "plaintext"],
237 |                             "deprecatedSupport": True,
238 |                             "preselectSupport": True,
239 |                             "tagSupport": {"valueSet": [1]},
240 |                             "insertReplaceSupport": False,
241 |                             "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]},
242 |                             "insertTextModeSupport": {"valueSet": [1, 2]},
243 |                             "labelDetailsSupport": True,
244 |                         },
245 |                         "insertTextMode": 2,
246 |                         "completionItemKind": {
247 |                             "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]
248 |                         },
249 |                         "completionList": {"itemDefaults": ["commitCharacters", "editRange", "insertTextFormat", "insertTextMode"]},
250 |                     },
251 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
252 |                     "signatureHelp": {
253 |                         "dynamicRegistration": True,
254 |                         "signatureInformation": {
255 |                             "documentationFormat": ["markdown", "plaintext"],
256 |                             "parameterInformation": {"labelOffsetSupport": True},
257 |                             "activeParameterSupport": True,
258 |                         },
259 |                         "contextSupport": True,
260 |                     },
261 |                     "definition": {"dynamicRegistration": True, "linkSupport": True},
262 |                     "references": {"dynamicRegistration": True},
263 |                     "documentHighlight": {"dynamicRegistration": True},
264 |                     "documentSymbol": {
265 |                         "dynamicRegistration": True,
266 |                         "symbolKind": {"valueSet": list(range(1, 27))},
267 |                         "hierarchicalDocumentSymbolSupport": True,
268 |                         "tagSupport": {"valueSet": [1]},
269 |                         "labelSupport": True,
270 |                     },
271 |                     "codeAction": {
272 |                         "dynamicRegistration": True,
273 |                         "isPreferredSupport": True,
274 |                         "disabledSupport": True,
275 |                         "dataSupport": True,
276 |                         "resolveSupport": {"properties": ["edit"]},
277 |                         "codeActionLiteralSupport": {
278 |                             "codeActionKind": {
279 |                                 "valueSet": [
280 |                                     "",
281 |                                     "quickfix",
282 |                                     "refactor",
283 |                                     "refactor.extract",
284 |                                     "refactor.inline",
285 |                                     "refactor.rewrite",
286 |                                     "source",
287 |                                     "source.organizeImports",
288 |                                 ]
289 |                             }
290 |                         },
291 |                         "honorsChangeAnnotations": False,
292 |                     },
293 |                     "codeLens": {"dynamicRegistration": True},
294 |                     "formatting": {"dynamicRegistration": True},
295 |                     "rangeFormatting": {"dynamicRegistration": True},
296 |                     "onTypeFormatting": {"dynamicRegistration": True},
297 |                     "rename": {
298 |                         "dynamicRegistration": True,
299 |                         "prepareSupport": True,
300 |                         "prepareSupportDefaultBehavior": 1,
301 |                         "honorsChangeAnnotations": True,
302 |                     },
303 |                     "documentLink": {"dynamicRegistration": True, "tooltipSupport": True},
304 |                     "typeDefinition": {"dynamicRegistration": True, "linkSupport": True},
305 |                     "implementation": {"dynamicRegistration": True, "linkSupport": True},
306 |                     "colorProvider": {"dynamicRegistration": True},
307 |                     "foldingRange": {
308 |                         "dynamicRegistration": True,
309 |                         "rangeLimit": 5000,
310 |                         "lineFoldingOnly": True,
311 |                         "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]},
312 |                         "foldingRange": {"collapsedText": False},
313 |                     },
314 |                     "declaration": {"dynamicRegistration": True, "linkSupport": True},
315 |                     "selectionRange": {"dynamicRegistration": True},
316 |                     "callHierarchy": {"dynamicRegistration": True},
317 |                     "semanticTokens": {
318 |                         "dynamicRegistration": True,
319 |                         "tokenTypes": [
320 |                             "namespace",
321 |                             "type",
322 |                             "class",
323 |                             "enum",
324 |                             "interface",
325 |                             "struct",
326 |                             "typeParameter",
327 |                             "parameter",
328 |                             "variable",
329 |                             "property",
330 |                             "enumMember",
331 |                             "event",
332 |                             "function",
333 |                             "method",
334 |                             "macro",
335 |                             "keyword",
336 |                             "modifier",
337 |                             "comment",
338 |                             "string",
339 |                             "number",
340 |                             "regexp",
341 |                             "operator",
342 |                             "decorator",
343 |                         ],
344 |                         "tokenModifiers": [
345 |                             "declaration",
346 |                             "definition",
347 |                             "readonly",
348 |                             "static",
349 |                             "deprecated",
350 |                             "abstract",
351 |                             "async",
352 |                             "modification",
353 |                             "documentation",
354 |                             "defaultLibrary",
355 |                         ],
356 |                         "formats": ["relative"],
357 |                         "requests": {"range": True, "full": {"delta": True}},
358 |                         "multilineTokenSupport": False,
359 |                         "overlappingTokenSupport": False,
360 |                         "serverCancelSupport": True,
361 |                         "augmentsSyntaxTokens": True,
362 |                     },
363 |                     "linkedEditingRange": {"dynamicRegistration": True},
364 |                     "typeHierarchy": {"dynamicRegistration": True},
365 |                     "inlineValue": {"dynamicRegistration": True},
366 |                     "inlayHint": {
367 |                         "dynamicRegistration": True,
368 |                         "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]},
369 |                     },
370 |                     "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
371 |                 },
372 |                 "window": {
373 |                     "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
374 |                     "showDocument": {"support": True},
375 |                     "workDoneProgress": True,
376 |                 },
377 |                 "general": {
378 |                     "staleRequestSupport": {
379 |                         "cancel": True,
380 |                         "retryOnContentModified": [
381 |                             "textDocument/semanticTokens/full",
382 |                             "textDocument/semanticTokens/range",
383 |                             "textDocument/semanticTokens/full/delta",
384 |                         ],
385 |                     },
386 |                     "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"},
387 |                     "markdown": {"parser": "marked", "version": "1.1.0"},
388 |                     "positionEncodings": ["utf-16"],
389 |                 },
390 |                 "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
391 |             },
392 |             "initializationOptions": {
393 |                 "workspaceFolders": [root_uri],
394 |                 "storagePath": None,
395 |                 "codegen": {"enabled": False},
396 |                 "compiler": {"jvm": {"target": "default"}},
397 |                 "completion": {"snippets": {"enabled": True}},
398 |                 "diagnostics": {"enabled": True, "level": 4, "debounceTime": 250},
399 |                 "scripts": {"enabled": True, "buildScriptsEnabled": True},
400 |                 "indexing": {"enabled": True},
401 |                 "externalSources": {"useKlsScheme": False, "autoConvertToKotlin": False},
402 |                 "inlayHints": {"typeHints": False, "parameterHints": False, "chainedHints": False},
403 |                 "formatting": {
404 |                     "formatter": "ktfmt",
405 |                     "ktfmt": {
406 |                         "style": "google",
407 |                         "indent": 4,
408 |                         "maxWidth": 100,
409 |                         "continuationIndent": 8,
410 |                         "removeUnusedImports": True,
411 |                     },
412 |                 },
413 |             },
414 |             "trace": "verbose",
415 |             "processId": os.getpid(),
416 |             "workspaceFolders": [
417 |                 {
418 |                     "uri": root_uri,
419 |                     "name": os.path.basename(repository_absolute_path),
420 |                 }
421 |             ],
422 |         }
423 |         return initialize_params
424 | 
425 |     def _start_server(self):
426 |         """
427 |         Starts the Kotlin Language Server
428 |         """
429 | 
430 |         def execute_client_command_handler(params):
431 |             return []
432 | 
433 |         def do_nothing(params):
434 |             return
435 | 
436 |         def window_log_message(msg):
437 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
438 | 
439 |         self.server.on_request("client/registerCapability", do_nothing)
440 |         self.server.on_notification("language/status", do_nothing)
441 |         self.server.on_notification("window/logMessage", window_log_message)
442 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
443 |         self.server.on_notification("$/progress", do_nothing)
444 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
445 |         self.server.on_notification("language/actionableNotification", do_nothing)
446 | 
447 |         self.logger.log("Starting Kotlin server process", logging.INFO)
448 |         self.server.start()
449 |         initialize_params = self._get_initialize_params(self.repository_root_path)
450 | 
451 |         self.logger.log(
452 |             "Sending initialize request from LSP client to LSP server and awaiting response",
453 |             logging.INFO,
454 |         )
455 |         init_response = self.server.send.initialize(initialize_params)
456 | 
457 |         capabilities = init_response["capabilities"]
458 |         assert "textDocumentSync" in capabilities, "Server must support textDocumentSync"
459 |         assert "hoverProvider" in capabilities, "Server must support hover"
460 |         assert "completionProvider" in capabilities, "Server must support code completion"
461 |         assert "signatureHelpProvider" in capabilities, "Server must support signature help"
462 |         assert "definitionProvider" in capabilities, "Server must support go to definition"
463 |         assert "referencesProvider" in capabilities, "Server must support find references"
464 |         assert "documentSymbolProvider" in capabilities, "Server must support document symbols"
465 |         assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols"
466 |         assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens"
467 | 
468 |         self.server.notify.initialized({})
469 |         self.completions_available.set()
470 | 
```

--------------------------------------------------------------------------------
/src/serena/resources/dashboard/dashboard.js:
--------------------------------------------------------------------------------

```javascript
  1 | class LogMessage {
  2 |     constructor(message, toolNames) {
  3 |         message = this.escapeHtml(message);
  4 |         const logLevel = this.determineLogLevel(message);
  5 |         const highlightedMessage = this.highlightToolNames(message, toolNames);
  6 |         this.$elem = $('<div>').addClass('log-' + logLevel).html(highlightedMessage + '\n');
  7 |     }
  8 | 
  9 |     determineLogLevel(message) {
 10 |         if (message.startsWith('DEBUG')) {
 11 |             return 'debug';
 12 |         } else if (message.startsWith('INFO')) {
 13 |             return 'info';
 14 |         } else if (message.startsWith('WARNING')) {
 15 |             return 'warning';
 16 |         } else if (message.startsWith('ERROR')) {
 17 |             return 'error';
 18 |         } else {
 19 |             return 'default';
 20 |         }
 21 |     }
 22 | 
 23 |     highlightToolNames(message, toolNames) {
 24 |         let highlightedMessage = message;
 25 |         toolNames.forEach(function(toolName) {
 26 |             const regex = new RegExp('\\b' + toolName + '\\b', 'gi');
 27 |             highlightedMessage = highlightedMessage.replace(regex, '<span class="tool-name">' + toolName + '</span>');
 28 |         });
 29 |         return highlightedMessage;
 30 |     }
 31 | 
 32 |     escapeHtml (convertString) {
 33 |         if (typeof convertString !== 'string') return convertString; 
 34 | 
 35 |         const patterns = {
 36 |             '<'  : '&lt;',
 37 |             '>'  : '&gt;',
 38 |             '&'  : '&amp;',
 39 |             '"'  : '&quot;',
 40 |             '\'' : '&#x27;',
 41 |             '`'  : '&#x60;'
 42 |         };
 43 | 
 44 |         return convertString.replace(/[<>&"'`]/g, match => patterns[match]);
 45 |   };
 46 | }
 47 | 
 48 | class Dashboard {
 49 |     constructor() {
 50 |         let self = this;
 51 | 
 52 |         this.toolNames = [];
 53 |         this.currentMaxIdx = -1;
 54 |         this.pollInterval = null;
 55 |         this.failureCount = 0;
 56 |         this.$logContainer = $('#log-container');
 57 |         this.$errorContainer = $('#error-container');
 58 |         this.$loadButton = $('#load-logs');
 59 |         this.$shutdownButton = $('#shutdown');
 60 |         this.$toggleStats = $('#toggle-stats');
 61 |         this.$statsSection = $('#stats-section');
 62 |         this.$refreshStats = $('#refresh-stats');
 63 |         this.$clearStats = $('#clear-stats');
 64 |         this.$themeToggle = $('#theme-toggle');
 65 |         this.$themeIcon = $('#theme-icon');
 66 |         this.$themeText = $('#theme-text');
 67 | 
 68 |         this.countChart = null;
 69 |         this.tokensChart = null;
 70 |         this.inputChart = null;
 71 |         this.outputChart = null;
 72 | 
 73 |         // register event handlers
 74 |         this.$loadButton.click(this.loadLogs.bind(this));
 75 |         this.$shutdownButton.click(this.shutdown.bind(this));
 76 |         this.$toggleStats.click(this.toggleStats.bind(this));
 77 |         this.$refreshStats.click(this.loadStats.bind(this));
 78 |         this.$clearStats.click(this.clearStats.bind(this));
 79 |         this.$themeToggle.click(this.toggleTheme.bind(this));
 80 | 
 81 |         // initialize theme
 82 |         this.initializeTheme();
 83 | 
 84 |         // initialize the application
 85 |         this.loadToolNames().then(function() {
 86 |             // Load logs on page load after tool names are loaded
 87 |             self.loadLogs();
 88 |         });
 89 |     }
 90 | 
 91 |     displayLogMessage(message) {
 92 |         $('#log-container').append(new LogMessage(message, this.toolNames).$elem);
 93 |     }
 94 | 
 95 |     loadToolNames() {
 96 |         let self = this;
 97 |         return $.ajax({
 98 |             url: '/get_tool_names',
 99 |             type: 'GET',
100 |             success: function(response) {
101 |                 self.toolNames = response.tool_names || [];
102 |                 console.log('Loaded tool names:', self.toolNames);
103 |             },
104 |             error: function(xhr, status, error) {
105 |                 console.error('Error loading tool names:', error);
106 |             }
107 |         });
108 |     }
109 | 
110 |     updateTitle(activeProject) {
111 |         document.title = activeProject ? `${activeProject} – Serena Dashboard` : 'Serena Dashboard';
112 |     }
113 | 
114 |     loadLogs() {
115 |         console.log("Loading logs");
116 |         let self = this;
117 | 
118 |         // Disable button and show loading state
119 |         self.$loadButton.prop('disabled', true).text('Loading...');
120 |         self.$errorContainer.empty();
121 | 
122 |         // Make API call
123 |         $.ajax({
124 |             url: '/get_log_messages',
125 |             type: 'POST',
126 |             contentType: 'application/json',
127 |             data: JSON.stringify({
128 |                 start_idx: 0
129 |             }),
130 |             success: function(response) {
131 |                 // Clear existing logs
132 |                 self.$logContainer.empty();
133 | 
134 |                 // Update max_idx
135 |                 self.currentMaxIdx = response.max_idx || -1;
136 | 
137 |                 // Display each log message
138 |                 if (response.messages && response.messages.length > 0) {
139 |                     response.messages.forEach(function(message) {
140 |                         self.displayLogMessage(message);
141 |                     });
142 | 
143 |                     // Auto-scroll to bottom
144 |                     const logContainer = $('#log-container')[0];
145 |                     logContainer.scrollTop = logContainer.scrollHeight;
146 |                 } else {
147 |                     $('#log-container').html('<div class="loading">No log messages found.</div>');
148 |                 }
149 | 
150 |                 self.updateTitle(response.active_project);
151 | 
152 |                 // Start periodic polling for new logs
153 |                 self.startPeriodicPolling();
154 |             },
155 |             error: function(xhr, status, error) {
156 |                 console.error('Error loading logs:', error);
157 |                 self.$errorContainer.html('<div class="error-message">Error loading logs: ' +
158 |                     (xhr.responseJSON ? xhr.responseJSON.detail : error) + '</div>');
159 |             },
160 |             complete: function() {
161 |                 // Re-enable button
162 |                 self.$loadButton.prop('disabled', false).text('Reload Log');
163 |             }
164 |         });
165 |     }
166 | 
167 |     pollForNewLogs() {
168 |         let self = this;
169 |         console.log("Polling logs", this.currentMaxIdx);
170 |         $.ajax({
171 |             url: '/get_log_messages',
172 |             type: 'POST',
173 |             contentType: 'application/json',
174 |             data: JSON.stringify({
175 |                 start_idx: self.currentMaxIdx + 1
176 |             }),
177 |             success: function(response) {
178 |                 self.failureCount = 0;
179 |                 // Only append new messages if we have any
180 |                 if (response.messages && response.messages.length > 0) {
181 |                     let wasAtBottom = false;
182 |                     const logContainer = $('#log-container')[0];
183 | 
184 |                     // Check if user was at the bottom before adding new logs
185 |                     if (logContainer.scrollHeight > 0) {
186 |                         wasAtBottom = (logContainer.scrollTop + logContainer.clientHeight) >= (logContainer.scrollHeight - 10);
187 |                     }
188 | 
189 |                     // Append new messages
190 |                     response.messages.forEach(function(message) {
191 |                         self.displayLogMessage(message);
192 |                     });
193 | 
194 |                     // Update max_idx
195 |                     self.currentMaxIdx = response.max_idx || self.currentMaxIdx;
196 | 
197 |                     // Auto-scroll to bottom if user was already at bottom
198 |                     if (wasAtBottom) {
199 |                         logContainer.scrollTop = logContainer.scrollHeight;
200 |                     }
201 |                 } else {
202 |                     // Update max_idx even if no new messages
203 |                     self.currentMaxIdx = response.max_idx || self.currentMaxIdx;
204 |                 }
205 | 
206 |                 // Update window title with active project
207 |                 self.updateTitle(response.active_project);
208 |             },
209 |             error: function(xhr, status, error) {
210 |                 console.error('Error polling for new logs:', error);
211 |                 self.failureCount++;
212 |                 if (self.failureCount >= 3) {
213 |                     console.log('Server appears to be down, closing tab');
214 |                     window.close();
215 |                 }
216 |             }
217 |         });
218 |     }
219 | 
220 |     startPeriodicPolling() {
221 |         // Clear any existing interval
222 |         if (this.pollInterval) {
223 |             clearInterval(this.pollInterval);
224 |         }
225 | 
226 |         // Start polling every second (1000ms)
227 |         this.pollInterval = setInterval(this.pollForNewLogs.bind(this), 1000);
228 |     }
229 | 
230 |     toggleStats() {
231 |         if (this.$statsSection.is(':visible')) {
232 |             this.$statsSection.hide();
233 |             this.$toggleStats.text('Show Stats');
234 |         } else {
235 |             this.$statsSection.show();
236 |             this.$toggleStats.text('Hide Stats');
237 |             this.loadStats();
238 |         }
239 |     }
240 | 
241 |     loadStats() {
242 |         let self = this;
243 |         $.when(
244 |             $.ajax({ url: '/get_tool_stats', type: 'GET' }),
245 |             $.ajax({ url: '/get_token_count_estimator_name', type: 'GET' })
246 |         ).done(function(statsResp, estimatorResp) {
247 |             const stats = statsResp[0].stats;
248 |             const tokenCountEstimatorName = estimatorResp[0].token_count_estimator_name;
249 |             self.displayStats(stats, tokenCountEstimatorName);
250 |         }).fail(function() {
251 |             console.error('Error loading stats or estimator name');
252 |         });
253 |     }
254 | 
255 | 
256 |     clearStats() {
257 |         let self = this;
258 |         $.ajax({
259 |             url: '/clear_tool_stats',
260 |             type: 'POST',
261 |             success: function() {
262 |                 self.loadStats();
263 |             },
264 |             error: function(xhr, status, error) {
265 |                 console.error('Error clearing stats:', error);
266 |             }
267 |         });
268 |     }
269 | 
270 |     displayStats(stats, tokenCountEstimatorName) {
271 |         const names = Object.keys(stats);
272 |       // If no stats collected
273 |         if (names.length === 0) {
274 |             // hide summary, charts, estimator name
275 |             $('#stats-summary').hide();
276 |             $('#estimator-name').hide();
277 |             $('.charts-container').hide();
278 |             // show no-stats message
279 |             $('#no-stats-message').show();
280 |             return;
281 |         } else {
282 |             // Ensure everything is visible
283 |             $('#estimator-name').show();
284 |             $('#stats-summary').show();
285 |             $('.charts-container').show();
286 |             $('#no-stats-message').hide();
287 |         }
288 | 
289 |         $('#estimator-name').html(`<strong>Token count estimator:</strong> ${tokenCountEstimatorName}`);
290 | 
291 |         const counts = names.map(n => stats[n].num_times_called);
292 |         const inputTokens = names.map(n => stats[n].input_tokens);
293 |         const outputTokens = names.map(n => stats[n].output_tokens);
294 |         const totalTokens = names.map(n => stats[n].input_tokens + stats[n].output_tokens);
295 |         
296 |         // Calculate totals for summary table
297 |         const totalCalls = counts.reduce((sum, count) => sum + count, 0);
298 |         const totalInputTokens = inputTokens.reduce((sum, tokens) => sum + tokens, 0);
299 |         const totalOutputTokens = outputTokens.reduce((sum, tokens) => sum + tokens, 0);
300 |         
301 |         // Generate consistent colors for tools
302 |         const colors = this.generateColors(names.length);
303 | 
304 |         const countCtx = document.getElementById('count-chart');
305 |         const tokensCtx = document.getElementById('tokens-chart');
306 |         const inputCtx = document.getElementById('input-chart');
307 |         const outputCtx = document.getElementById('output-chart');
308 | 
309 |         if (this.countChart) this.countChart.destroy();
310 |         if (this.tokensChart) this.tokensChart.destroy();
311 |         if (this.inputChart) this.inputChart.destroy();
312 |         if (this.outputChart) this.outputChart.destroy();
313 | 
314 |         // Update summary table
315 |         this.updateSummaryTable(totalCalls, totalInputTokens, totalOutputTokens);
316 | 
317 |         // Register datalabels plugin
318 |         Chart.register(ChartDataLabels);
319 | 
320 |         // Get theme-aware colors
321 |         const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
322 |         const textColor = isDark ? '#ffffff' : '#000000';
323 |         const gridColor = isDark ? '#444' : '#ddd';
324 | 
325 |         // Tool calls pie chart
326 |         this.countChart = new Chart(countCtx, {
327 |             type: 'pie',
328 |             data: { 
329 |                 labels: names, 
330 |                 datasets: [{ 
331 |                     data: counts,
332 |                     backgroundColor: colors
333 |                 }] 
334 |             },
335 |             options: {
336 |                 plugins: {
337 |                     legend: { 
338 |                         display: true,
339 |                         labels: {
340 |                             color: textColor
341 |                         }
342 |                     },
343 |                     datalabels: {
344 |                         display: true,
345 |                         color: 'white',
346 |                         font: { weight: 'bold' },
347 |                         formatter: (value) => value
348 |                     }
349 |                 }
350 |             }
351 |         });
352 | 
353 |         // Input tokens pie chart
354 |         this.inputChart = new Chart(inputCtx, {
355 |             type: 'pie',
356 |             data: { 
357 |                 labels: names, 
358 |                 datasets: [{ 
359 |                     data: inputTokens,
360 |                     backgroundColor: colors
361 |                 }] 
362 |             },
363 |             options: {
364 |                 plugins: {
365 |                     legend: { 
366 |                         display: true,
367 |                         labels: {
368 |                             color: textColor
369 |                         }
370 |                     },
371 |                     datalabels: {
372 |                         display: true,
373 |                         color: 'white',
374 |                         font: { weight: 'bold' },
375 |                         formatter: (value) => value
376 |                     }
377 |                 }
378 |             }
379 |         });
380 | 
381 |         // Output tokens pie chart
382 |         this.outputChart = new Chart(outputCtx, {
383 |             type: 'pie',
384 |             data: { 
385 |                 labels: names, 
386 |                 datasets: [{ 
387 |                     data: outputTokens,
388 |                     backgroundColor: colors
389 |                 }] 
390 |             },
391 |             options: {
392 |                 plugins: {
393 |                     legend: { 
394 |                         display: true,
395 |                         labels: {
396 |                             color: textColor
397 |                         }
398 |                     },
399 |                     datalabels: {
400 |                         display: true,
401 |                         color: 'white',
402 |                         font: { weight: 'bold' },
403 |                         formatter: (value) => value
404 |                     }
405 |                 }
406 |             }
407 |         });
408 | 
409 |         // Combined input/output tokens bar chart
410 |         this.tokensChart = new Chart(tokensCtx, {
411 |             type: 'bar',
412 |             data: { 
413 |                 labels: names, 
414 |                 datasets: [
415 |                     { 
416 |                         label: 'Input Tokens', 
417 |                         data: inputTokens,
418 |                         backgroundColor: colors.map(color => color + '80'), // Semi-transparent
419 |                         borderColor: colors,
420 |                         borderWidth: 2,
421 |                         borderSkipped: false,
422 |                         yAxisID: 'y'
423 |                     },
424 |                     { 
425 |                         label: 'Output Tokens', 
426 |                         data: outputTokens,
427 |                         backgroundColor: colors,
428 |                         yAxisID: 'y1'
429 |                     }
430 |                 ]
431 |             },
432 |             options: {
433 |                 responsive: true,
434 |                 plugins: {
435 |                     legend: {
436 |                         labels: {
437 |                             color: textColor
438 |                         }
439 |                     }
440 |                 },
441 |                 scales: {
442 |                     x: {
443 |                         ticks: {
444 |                             color: textColor
445 |                         },
446 |                         grid: {
447 |                             color: gridColor
448 |                         }
449 |                     },
450 |                     y: {
451 |                         type: 'linear',
452 |                         display: true,
453 |                         position: 'left',
454 |                         beginAtZero: true,
455 |                         title: { 
456 |                             display: true, 
457 |                             text: 'Input Tokens',
458 |                             color: textColor
459 |                         },
460 |                         ticks: {
461 |                             color: textColor
462 |                         },
463 |                         grid: {
464 |                             color: gridColor
465 |                         }
466 |                     },
467 |                     y1: {
468 |                         type: 'linear',
469 |                         display: true,
470 |                         position: 'right',
471 |                         beginAtZero: true,
472 |                         title: { 
473 |                             display: true, 
474 |                             text: 'Output Tokens',
475 |                             color: textColor
476 |                         },
477 |                         ticks: {
478 |                             color: textColor
479 |                         },
480 |                         grid: { 
481 |                             drawOnChartArea: false,
482 |                             color: gridColor
483 |                         }
484 |                     }
485 |                 }
486 |             }
487 |         });
488 |     }
489 | 
490 |     generateColors(count) {
491 |         const colors = [
492 |             '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF',
493 |             '#FF9F40', '#FF6384', '#C9CBCF', '#4BC0C0', '#FF6384'
494 |         ];
495 |         return Array.from({length: count}, (_, i) => colors[i % colors.length]);
496 |     }
497 | 
498 |     updateSummaryTable(totalCalls, totalInputTokens, totalOutputTokens) {
499 |         const tableHtml = `
500 |             <table class="stats-summary">
501 |                 <tr><th>Metric</th><th>Total</th></tr>
502 |                 <tr><td>Tool Calls</td><td>${totalCalls}</td></tr>
503 |                 <tr><td>Input Tokens</td><td>${totalInputTokens}</td></tr>
504 |                 <tr><td>Output Tokens</td><td>${totalOutputTokens}</td></tr>
505 |                 <tr><td>Total Tokens</td><td>${totalInputTokens + totalOutputTokens}</td></tr>
506 |             </table>
507 |         `;
508 |         $('#stats-summary').html(tableHtml);
509 |     }
510 | 
511 |     initializeTheme() {
512 |         // Check if user has manually set a theme preference
513 |         const savedTheme = localStorage.getItem('serena-theme');
514 |         
515 |         if (savedTheme) {
516 |             // User has manually set a preference, use it
517 |             this.setTheme(savedTheme);
518 |         } else {
519 |             // No manual preference, detect system color scheme
520 |             this.detectSystemTheme();
521 |         }
522 |         
523 |         // Listen for system theme changes
524 |         this.setupSystemThemeListener();
525 |     }
526 | 
527 |     detectSystemTheme() {
528 |         // Check if system prefers dark mode
529 |         const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
530 |         const theme = prefersDark ? 'dark' : 'light';
531 |         this.setTheme(theme);
532 |     }
533 | 
534 |     setupSystemThemeListener() {
535 |         // Listen for changes in system color scheme
536 |         const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
537 |         
538 |         const handleSystemThemeChange = (e) => {
539 |             // Only auto-switch if user hasn't manually set a preference
540 |             const savedTheme = localStorage.getItem('serena-theme');
541 |             if (!savedTheme) {
542 |                 const newTheme = e.matches ? 'dark' : 'light';
543 |                 this.setTheme(newTheme);
544 |             }
545 |         };
546 |         
547 |         // Add listener for system theme changes
548 |         if (mediaQuery.addEventListener) {
549 |             mediaQuery.addEventListener('change', handleSystemThemeChange);
550 |         } else {
551 |             // Fallback for older browsers
552 |             mediaQuery.addListener(handleSystemThemeChange);
553 |         }
554 |     }
555 | 
556 |     toggleTheme() {
557 |         const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
558 |         const newTheme = currentTheme === 'light' ? 'dark' : 'light';
559 |         
560 |         // When user manually toggles, save their preference
561 |         localStorage.setItem('serena-theme', newTheme);
562 |         this.setTheme(newTheme);
563 |     }
564 | 
565 |     setTheme(theme) {
566 |         // Set the theme on the document element
567 |         document.documentElement.setAttribute('data-theme', theme);
568 |         
569 |         // Update the toggle button
570 |         if (theme === 'dark') {
571 |             this.$themeIcon.text('☀️');
572 |             this.$themeText.text('Light');
573 |         } else {
574 |             this.$themeIcon.text('🌙');
575 |             this.$themeText.text('Dark');
576 |         }
577 |         
578 |         // Update the logo based on theme
579 |         this.updateLogo(theme);
580 |         
581 |         // Save to localStorage
582 |         localStorage.setItem('serena-theme', theme);
583 |         
584 |         // Update charts if they exist
585 |         this.updateChartsTheme();
586 |     }
587 | 
588 |     updateLogo(theme) {
589 |         const logoElement = document.getElementById('serena-logo');
590 |         if (logoElement) {
591 |             if (theme === 'dark') {
592 |                 logoElement.src = 'serena-logs-dark-mode.png';
593 |             } else {
594 |                 logoElement.src = 'serena-logs.png';
595 |             }
596 |         }
597 |     }
598 | 
599 |     updateChartsTheme() {
600 |         const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
601 |         const textColor = isDark ? '#ffffff' : '#000000';
602 |         const gridColor = isDark ? '#444' : '#ddd';
603 |         
604 |         // Update existing charts
605 |         if (this.countChart) {
606 |             this.countChart.options.scales.x.ticks.color = textColor;
607 |             this.countChart.options.scales.y.ticks.color = textColor;
608 |             this.countChart.options.scales.x.grid.color = gridColor;
609 |             this.countChart.options.scales.y.grid.color = gridColor;
610 |             this.countChart.update();
611 |         }
612 |         
613 |         if (this.inputChart) {
614 |             this.inputChart.options.scales.x.ticks.color = textColor;
615 |             this.inputChart.options.scales.y.ticks.color = textColor;
616 |             this.inputChart.options.scales.x.grid.color = gridColor;
617 |             this.inputChart.options.scales.y.grid.color = gridColor;
618 |             this.inputChart.update();
619 |         }
620 |         
621 |         if (this.outputChart) {
622 |             this.outputChart.options.scales.x.ticks.color = textColor;
623 |             this.outputChart.options.scales.y.ticks.color = textColor;
624 |             this.outputChart.options.scales.x.grid.color = gridColor;
625 |             this.outputChart.options.scales.y.grid.color = gridColor;
626 |             this.outputChart.update();
627 |         }
628 |         
629 |         if (this.tokensChart) {
630 |             this.tokensChart.options.scales.x.ticks.color = textColor;
631 |             this.tokensChart.options.scales.y.ticks.color = textColor;
632 |             this.tokensChart.options.scales.y1.ticks.color = textColor;
633 |             this.tokensChart.options.scales.x.grid.color = gridColor;
634 |             this.tokensChart.options.scales.y.grid.color = gridColor;
635 |             this.tokensChart.options.scales.y1.grid.color = gridColor;
636 |             this.tokensChart.update();
637 |         }
638 |     }
639 | 
640 |     shutdown() {
641 |         const self = this;
642 |         const _shutdown = function () {
643 |             console.log("Triggering shutdown");
644 |             $.ajax({
645 |                 url: '/shutdown',
646 |                 type: "PUT",
647 |                 contentType: 'application/json',
648 |             });
649 |             self.$errorContainer.html('<div class="error-message">Shutting down ...</div>')
650 |             setTimeout(function() {
651 |                 window.close();
652 |             }, 2000);
653 |         }
654 | 
655 |         // ask for confirmation using a dialog
656 |         if (confirm("This will fully terminate the Serena server.")) {
657 |             _shutdown();
658 |         } else {
659 |             console.log("Shutdown cancelled");
660 |         }
661 |     }
662 | }
663 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/ls_handler.py:
--------------------------------------------------------------------------------

```python
  1 | import asyncio
  2 | import json
  3 | import logging
  4 | import os
  5 | import platform
  6 | import subprocess
  7 | import threading
  8 | import time
  9 | from collections.abc import Callable
 10 | from dataclasses import dataclass
 11 | from queue import Empty, Queue
 12 | from typing import Any
 13 | 
 14 | import psutil
 15 | from sensai.util.string import ToStringMixin
 16 | 
 17 | from solidlsp.ls_exceptions import SolidLSPException
 18 | from solidlsp.ls_request import LanguageServerRequest
 19 | from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification
 20 | from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes
 21 | from solidlsp.lsp_protocol_handler.server import (
 22 |     ENCODING,
 23 |     LSPError,
 24 |     MessageType,
 25 |     PayloadLike,
 26 |     ProcessLaunchInfo,
 27 |     StringDict,
 28 |     content_length,
 29 |     create_message,
 30 |     make_error_response,
 31 |     make_notification,
 32 |     make_request,
 33 |     make_response,
 34 | )
 35 | from solidlsp.util.subprocess_util import subprocess_kwargs
 36 | 
 37 | log = logging.getLogger(__name__)
 38 | 
 39 | 
 40 | class LanguageServerTerminatedException(Exception):
 41 |     """
 42 |     Exception raised when the language server process has terminated unexpectedly.
 43 |     """
 44 | 
 45 |     def __init__(self, message: str, cause: Exception | None = None) -> None:
 46 |         super().__init__(message)
 47 |         self.message = message
 48 |         self.cause = cause
 49 | 
 50 |     def __str__(self) -> str:
 51 |         return f"LanguageServerTerminatedException: {self.message}" + (f"; Cause: {self.cause}" if self.cause else "")
 52 | 
 53 | 
 54 | class Request(ToStringMixin):
 55 | 
 56 |     @dataclass
 57 |     class Result:
 58 |         payload: PayloadLike | None = None
 59 |         error: Exception | None = None
 60 | 
 61 |         def is_error(self) -> bool:
 62 |             return self.error is not None
 63 | 
 64 |     def __init__(self, request_id: int, method: str) -> None:
 65 |         self._request_id = request_id
 66 |         self._method = method
 67 |         self._status = "pending"
 68 |         self._result_queue = Queue()
 69 | 
 70 |     def _tostring_includes(self) -> list[str]:
 71 |         return ["_request_id", "_status", "_method"]
 72 | 
 73 |     def on_result(self, params: PayloadLike) -> None:
 74 |         self._status = "completed"
 75 |         self._result_queue.put(Request.Result(payload=params))
 76 | 
 77 |     def on_error(self, err: Exception) -> None:
 78 |         """
 79 |         :param err: the error that occurred while processing the request (typically an LSPError
 80 |             for errors returned by the LS or LanguageServerTerminatedException if the error
 81 |             is due to the language server process terminating unexpectedly).
 82 |         """
 83 |         self._status = "error"
 84 |         self._result_queue.put(Request.Result(error=err))
 85 | 
 86 |     def get_result(self, timeout: float | None = None) -> Result:
 87 |         try:
 88 |             return self._result_queue.get(timeout=timeout)
 89 |         except Empty as e:
 90 |             if timeout is not None:
 91 |                 raise TimeoutError(f"Request timed out ({timeout=})") from e
 92 |             raise e
 93 | 
 94 | 
 95 | class SolidLanguageServerHandler:
 96 |     """
 97 |     This class provides the implementation of Python client for the Language Server Protocol.
 98 |     A class that launches the language server and communicates with it
 99 |     using the Language Server Protocol (LSP).
100 | 
101 |     It provides methods for sending requests, responses, and notifications to the server
102 |     and for registering handlers for requests and notifications from the server.
103 | 
104 |     Uses JSON-RPC 2.0 for communication with the server over stdin/stdout.
105 | 
106 |     Attributes:
107 |         send: A LspRequest object that can be used to send requests to the server and
108 |             await for the responses.
109 |         notify: A LspNotification object that can be used to send notifications to the server.
110 |         cmd: A string that represents the command to launch the language server process.
111 |         process: A subprocess.Popen object that represents the language server process.
112 |         request_id: An integer that represents the next available request id for the client.
113 |         _pending_requests: A dictionary that maps request ids to Request objects that
114 |             store the results or errors of the requests.
115 |         on_request_handlers: A dictionary that maps method names to callback functions
116 |             that handle requests from the server.
117 |         on_notification_handlers: A dictionary that maps method names to callback functions
118 |             that handle notifications from the server.
119 |         logger: An optional function that takes two strings (source and destination) and
120 |             a payload dictionary, and logs the communication between the client and the server.
121 |         tasks: A dictionary that maps task ids to asyncio.Task objects that represent
122 |             the asynchronous tasks created by the handler.
123 |         task_counter: An integer that represents the next available task id for the handler.
124 |         loop: An asyncio.AbstractEventLoop object that represents the event loop used by the handler.
125 |         start_independent_lsp_process: An optional boolean flag that indicates whether to start the
126 |         language server process in an independent process group. Default is `True`. Setting it to
127 |         `False` means that the language server process will be in the same process group as the
128 |         the current process, and any SIGINT and SIGTERM signals will be sent to both processes.
129 | 
130 |     """
131 | 
132 |     def __init__(
133 |         self,
134 |         process_launch_info: ProcessLaunchInfo,
135 |         logger: Callable[[str, str, StringDict | str], None] | None = None,
136 |         start_independent_lsp_process=True,
137 |         request_timeout: float | None = None,
138 |     ) -> None:
139 |         self.send = LanguageServerRequest(self)
140 |         self.notify = LspNotification(self.send_notification)
141 | 
142 |         self.process_launch_info = process_launch_info
143 |         self.process: subprocess.Popen | None = None
144 |         self._is_shutting_down = False
145 | 
146 |         self.request_id = 1
147 |         self._pending_requests: dict[Any, Request] = {}
148 |         self.on_request_handlers = {}
149 |         self.on_notification_handlers = {}
150 |         self.logger = logger
151 |         self.tasks = {}
152 |         self.task_counter = 0
153 |         self.loop = None
154 |         self.start_independent_lsp_process = start_independent_lsp_process
155 |         self._request_timeout = request_timeout
156 | 
157 |         # Add thread locks for shared resources to prevent race conditions
158 |         self._stdin_lock = threading.Lock()
159 |         self._request_id_lock = threading.Lock()
160 |         self._response_handlers_lock = threading.Lock()
161 |         self._tasks_lock = threading.Lock()
162 | 
163 |     def set_request_timeout(self, timeout: float | None) -> None:
164 |         """
165 |         :param timeout: the timeout, in seconds, for all requests sent to the language server.
166 |         """
167 |         self._request_timeout = timeout
168 | 
169 |     def is_running(self) -> bool:
170 |         """
171 |         Checks if the language server process is currently running.
172 |         """
173 |         return self.process is not None and self.process.returncode is None
174 | 
175 |     def start(self) -> None:
176 |         """
177 |         Starts the language server process and creates a task to continuously read from its stdout to handle communications
178 |         from the server to the client
179 |         """
180 |         child_proc_env = os.environ.copy()
181 |         child_proc_env.update(self.process_launch_info.env)
182 | 
183 |         cmd = self.process_launch_info.cmd
184 |         is_windows = platform.system() == "Windows"
185 |         if not isinstance(cmd, str) and not is_windows:
186 |             # Since we are using the shell, we need to convert the command list to a single string
187 |             # on Linux/macOS
188 |             cmd = " ".join(cmd)
189 |         log.info("Starting language server process via command: %s", self.process_launch_info.cmd)
190 |         kwargs = subprocess_kwargs()
191 |         kwargs["start_new_session"] = self.start_independent_lsp_process
192 |         self.process = subprocess.Popen(
193 |             cmd,
194 |             stdout=subprocess.PIPE,
195 |             stdin=subprocess.PIPE,
196 |             stderr=subprocess.PIPE,
197 |             env=child_proc_env,
198 |             cwd=self.process_launch_info.cwd,
199 |             shell=True,
200 |             **kwargs,
201 |         )
202 | 
203 |         # Check if process terminated immediately
204 |         if self.process.returncode is not None:
205 |             log.error("Language server has already terminated/could not be started")
206 |             # Process has already terminated
207 |             stderr_data = self.process.stderr.read()
208 |             error_message = stderr_data.decode("utf-8", errors="replace")
209 |             raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}")
210 | 
211 |         # start threads to read stdout and stderr of the process
212 |         threading.Thread(
213 |             target=self._read_ls_process_stdout,
214 |             name="LSP-stdout-reader",
215 |             daemon=True,
216 |         ).start()
217 |         threading.Thread(
218 |             target=self._read_ls_process_stderr,
219 |             name="LSP-stderr-reader",
220 |             daemon=True,
221 |         ).start()
222 | 
223 |     def stop(self) -> None:
224 |         """
225 |         Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary
226 |         """
227 |         process = self.process
228 |         self.process = None
229 |         if process:
230 |             self._cleanup_process(process)
231 | 
232 |     def _cleanup_process(self, process):
233 |         """Clean up a process: close stdin, terminate/kill process, close stdout/stderr."""
234 |         # Close stdin first to prevent deadlocks
235 |         # See: https://bugs.python.org/issue35539
236 |         self._safely_close_pipe(process.stdin)
237 | 
238 |         # Terminate/kill the process if it's still running
239 |         if process.returncode is None:
240 |             self._terminate_or_kill_process(process)
241 | 
242 |         # Close stdout and stderr pipes after process has exited
243 |         # This is essential to prevent "I/O operation on closed pipe" errors and
244 |         # "Event loop is closed" errors during garbage collection
245 |         # See: https://bugs.python.org/issue41320 and https://github.com/python/cpython/issues/88050
246 |         self._safely_close_pipe(process.stdout)
247 |         self._safely_close_pipe(process.stderr)
248 | 
249 |     def _safely_close_pipe(self, pipe):
250 |         """Safely close a pipe, ignoring any exceptions."""
251 |         if pipe:
252 |             try:
253 |                 pipe.close()
254 |             except Exception:
255 |                 pass
256 | 
257 |     def _terminate_or_kill_process(self, process):
258 |         """Try to terminate the process gracefully, then forcefully if necessary."""
259 |         # First try to terminate the process tree gracefully
260 |         self._signal_process_tree(process, terminate=True)
261 | 
262 |     def _signal_process_tree(self, process, terminate=True):
263 |         """Send signal (terminate or kill) to the process and all its children."""
264 |         signal_method = "terminate" if terminate else "kill"
265 | 
266 |         # Try to get the parent process
267 |         parent = None
268 |         try:
269 |             parent = psutil.Process(process.pid)
270 |         except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
271 |             pass
272 | 
273 |         # If we have the parent process and it's running, signal the entire tree
274 |         if parent and parent.is_running():
275 |             # Signal children first
276 |             for child in parent.children(recursive=True):
277 |                 try:
278 |                     getattr(child, signal_method)()
279 |                 except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
280 |                     pass
281 | 
282 |             # Then signal the parent
283 |             try:
284 |                 getattr(parent, signal_method)()
285 |             except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
286 |                 pass
287 |         else:
288 |             # Fall back to direct process signaling
289 |             try:
290 |                 getattr(process, signal_method)()
291 |             except Exception:
292 |                 pass
293 | 
294 |     def shutdown(self) -> None:
295 |         """
296 |         Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit
297 |         """
298 |         self._is_shutting_down = True
299 |         self._log("Sending shutdown request to server")
300 |         self.send.shutdown()
301 |         self._log("Received shutdown response from server")
302 |         self._log("Sending exit notification to server")
303 |         self.notify.exit()
304 |         self._log("Sent exit notification to server")
305 | 
306 |     def _log(self, message: str | StringDict) -> None:
307 |         """
308 |         Create a log message
309 |         """
310 |         if self.logger is not None:
311 |             self.logger("client", "logger", message)
312 | 
313 |     @staticmethod
314 |     def _read_bytes_from_process(process, stream, num_bytes):
315 |         """Read exactly num_bytes from process stdout"""
316 |         data = b""
317 |         while len(data) < num_bytes:
318 |             chunk = stream.read(num_bytes - len(data))
319 |             if not chunk:
320 |                 if process.poll() is not None:
321 |                     raise LanguageServerTerminatedException(
322 |                         f"Process terminated while trying to read response (read {num_bytes} of {len(data)} bytes before termination)"
323 |                     )
324 |                 # Process still running but no data available yet, retry after a short delay
325 |                 time.sleep(0.01)
326 |                 continue
327 |             data += chunk
328 |         return data
329 | 
330 |     def _read_ls_process_stdout(self) -> None:
331 |         """
332 |         Continuously read from the language server process stdout and handle the messages
333 |         invoking the registered response and notification handlers
334 |         """
335 |         exception: Exception | None = None
336 |         try:
337 |             while self.process and self.process.stdout:
338 |                 if self.process.poll() is not None:  # process has terminated
339 |                     break
340 |                 line = self.process.stdout.readline()
341 |                 if not line:
342 |                     continue
343 |                 try:
344 |                     num_bytes = content_length(line)
345 |                 except ValueError:
346 |                     continue
347 |                 if num_bytes is None:
348 |                     continue
349 |                 while line and line.strip():
350 |                     line = self.process.stdout.readline()
351 |                 if not line:
352 |                     continue
353 |                 body = self._read_bytes_from_process(self.process, self.process.stdout, num_bytes)
354 | 
355 |                 self._handle_body(body)
356 |         except LanguageServerTerminatedException as e:
357 |             exception = e
358 |         except (BrokenPipeError, ConnectionResetError) as e:
359 |             exception = LanguageServerTerminatedException("Language server process terminated while reading stdout", cause=e)
360 |         except Exception as e:
361 |             exception = LanguageServerTerminatedException("Unexpected error while reading stdout from language server process", cause=e)
362 |         log.info("Language server stdout reader thread has terminated")
363 |         if not self._is_shutting_down:
364 |             if exception is None:
365 |                 exception = LanguageServerTerminatedException("Language server stdout read process terminated unexpectedly")
366 |             log.error(str(exception))
367 |             self._cancel_pending_requests(exception)
368 | 
369 |     def _read_ls_process_stderr(self) -> None:
370 |         """
371 |         Continuously read from the language server process stderr and log the messages
372 |         """
373 |         try:
374 |             while self.process and self.process.stderr:
375 |                 if self.process.poll() is not None:
376 |                     # process has terminated
377 |                     break
378 |                 line = self.process.stderr.readline()
379 |                 if not line:
380 |                     continue
381 |                 line = line.decode(ENCODING, errors="replace")
382 |                 line_lower = line.lower()
383 |                 if "error" in line_lower or "exception" in line_lower or line.startswith("E["):
384 |                     level = logging.ERROR
385 |                 else:
386 |                     level = logging.INFO
387 |                 log.log(level, line)
388 |         except Exception as e:
389 |             log.error("Error while reading stderr from language server process: %s", e, exc_info=e)
390 |         if not self._is_shutting_down:
391 |             log.error("Language server stderr reader thread terminated unexpectedly")
392 |         else:
393 |             log.info("Language server stderr reader thread has terminated")
394 | 
395 |     def _handle_body(self, body: bytes) -> None:
396 |         """
397 |         Parse the body text received from the language server process and invoke the appropriate handler
398 |         """
399 |         try:
400 |             self._receive_payload(json.loads(body))
401 |         except OSError as ex:
402 |             self._log(f"malformed {ENCODING}: {ex}")
403 |         except UnicodeDecodeError as ex:
404 |             self._log(f"malformed {ENCODING}: {ex}")
405 |         except json.JSONDecodeError as ex:
406 |             self._log(f"malformed JSON: {ex}")
407 | 
408 |     def _receive_payload(self, payload: StringDict) -> None:
409 |         """
410 |         Determine if the payload received from server is for a request, response, or notification and invoke the appropriate handler
411 |         """
412 |         if self.logger:
413 |             self.logger("server", "client", payload)
414 |         try:
415 |             if "method" in payload:
416 |                 if "id" in payload:
417 |                     self._request_handler(payload)
418 |                 else:
419 |                     self._notification_handler(payload)
420 |             elif "id" in payload:
421 |                 self._response_handler(payload)
422 |             else:
423 |                 self._log(f"Unknown payload type: {payload}")
424 |         except Exception as err:
425 |             self._log(f"Error handling server payload: {err}")
426 | 
427 |     def send_notification(self, method: str, params: dict | None = None) -> None:
428 |         """
429 |         Send notification pertaining to the given method to the server with the given parameters
430 |         """
431 |         self._send_payload(make_notification(method, params))
432 | 
433 |     def send_response(self, request_id: Any, params: PayloadLike) -> None:
434 |         """
435 |         Send response to the given request id to the server with the given parameters
436 |         """
437 |         self._send_payload(make_response(request_id, params))
438 | 
439 |     def send_error_response(self, request_id: Any, err: LSPError) -> None:
440 |         """
441 |         Send error response to the given request id to the server with the given error
442 |         """
443 |         # Use lock to prevent race conditions on tasks and task_counter
444 |         self._send_payload(make_error_response(request_id, err))
445 | 
446 |     def _cancel_pending_requests(self, exception: Exception) -> None:
447 |         """
448 |         Cancel all pending requests by setting their results to an error
449 |         """
450 |         with self._response_handlers_lock:
451 |             log.info("Cancelling %d pending language server requests", len(self._pending_requests))
452 |             for request in self._pending_requests.values():
453 |                 log.info("Cancelling %s", request)
454 |                 request.on_error(exception)
455 |             self._pending_requests.clear()
456 | 
457 |     def send_request(self, method: str, params: dict | None = None) -> PayloadLike:
458 |         """
459 |         Send request to the server, register the request id, and wait for the response
460 |         """
461 |         with self._request_id_lock:
462 |             request_id = self.request_id
463 |             self.request_id += 1
464 | 
465 |         request = Request(request_id=request_id, method=method)
466 |         log.debug("Starting: %s", request)
467 | 
468 |         with self._response_handlers_lock:
469 |             self._pending_requests[request_id] = request
470 | 
471 |         self._send_payload(make_request(method, request_id, params))
472 | 
473 |         self._log(f"Waiting for response to request {method} with params:\n{params}")
474 |         result = request.get_result(timeout=self._request_timeout)
475 |         log.debug("Completed: %s", request)
476 | 
477 |         self._log("Processing result")
478 |         if result.is_error():
479 |             raise SolidLSPException(f"Error processing request {method} with params:\n{params}", cause=result.error) from result.error
480 | 
481 |         self._log(f"Returning non-error result, which is:\n{result.payload}")
482 |         return result.payload
483 | 
484 |     def _send_payload(self, payload: StringDict) -> None:
485 |         """
486 |         Send the payload to the server by writing to its stdin asynchronously.
487 |         """
488 |         if not self.process or not self.process.stdin:
489 |             return
490 |         self._log(payload)
491 |         msg = create_message(payload)
492 | 
493 |         # Use lock to prevent concurrent writes to stdin that cause buffer corruption
494 |         with self._stdin_lock:
495 |             try:
496 |                 self.process.stdin.writelines(msg)
497 |                 self.process.stdin.flush()
498 |             except (BrokenPipeError, ConnectionResetError, OSError) as e:
499 |                 # Log the error but don't raise to prevent cascading failures
500 |                 if self.logger:
501 |                     self.logger("client", "logger", f"Failed to write to stdin: {e}")
502 |                 return
503 | 
504 |     def on_request(self, method: str, cb) -> None:
505 |         """
506 |         Register the callback function to handle requests from the server to the client for the given method
507 |         """
508 |         self.on_request_handlers[method] = cb
509 | 
510 |     def on_notification(self, method: str, cb) -> None:
511 |         """
512 |         Register the callback function to handle notifications from the server to the client for the given method
513 |         """
514 |         self.on_notification_handlers[method] = cb
515 | 
516 |     def _response_handler(self, response: StringDict) -> None:
517 |         """
518 |         Handle the response received from the server for a request, using the id to determine the request
519 |         """
520 |         response_id = response["id"]
521 |         with self._response_handlers_lock:
522 |             request = self._pending_requests.pop(response_id, None)
523 |             if request is None and isinstance(response_id, str) and response_id.isdigit():
524 |                 request = self._pending_requests.pop(int(response_id), None)
525 | 
526 |             if request is None:  # need to convert response_id to the right type
527 |                 log.debug("Request interrupted by user or not found for ID %s", response_id)
528 |                 return
529 | 
530 |         if "result" in response and "error" not in response:
531 |             request.on_result(response["result"])
532 |         elif "result" not in response and "error" in response:
533 |             request.on_error(LSPError.from_lsp(response["error"]))
534 |         else:
535 |             request.on_error(LSPError(ErrorCodes.InvalidRequest, ""))
536 | 
537 |     def _request_handler(self, response: StringDict) -> None:
538 |         """
539 |         Handle the request received from the server: call the appropriate callback function and return the result
540 |         """
541 |         method = response.get("method", "")
542 |         params = response.get("params")
543 |         request_id = response.get("id")
544 |         handler = self.on_request_handlers.get(method)
545 |         if not handler:
546 |             self.send_error_response(
547 |                 request_id,
548 |                 LSPError(
549 |                     ErrorCodes.MethodNotFound,
550 |                     f"method '{method}' not handled on client.",
551 |                 ),
552 |             )
553 |             return
554 |         try:
555 |             self.send_response(request_id, handler(params))
556 |         except LSPError as ex:
557 |             self.send_error_response(request_id, ex)
558 |         except Exception as ex:
559 |             self.send_error_response(request_id, LSPError(ErrorCodes.InternalError, str(ex)))
560 | 
561 |     def _notification_handler(self, response: StringDict) -> None:
562 |         """
563 |         Handle the notification received from the server: call the appropriate callback function
564 |         """
565 |         method = response.get("method", "")
566 |         params = response.get("params")
567 |         handler = self.on_notification_handlers.get(method)
568 |         if not handler:
569 |             self._log(f"unhandled {method}")
570 |             return
571 |         try:
572 |             handler(params)
573 |         except asyncio.CancelledError:
574 |             return
575 |         except Exception as ex:
576 |             if (not self._is_shutting_down) and self.logger:
577 |                 self.logger(
578 |                     "client",
579 |                     "logger",
580 |                     str(
581 |                         {
582 |                             "type": MessageType.error,
583 |                             "message": str(ex),
584 |                             "method": method,
585 |                             "params": params,
586 |                         }
587 |                     ),
588 |                 )
589 | 
```

--------------------------------------------------------------------------------
/test/serena/util/test_file_system.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | import shutil
  3 | import tempfile
  4 | from pathlib import Path
  5 | 
  6 | # Assuming the gitignore parser code is in a module named 'gitignore_parser'
  7 | from serena.util.file_system import GitignoreParser, GitignoreSpec
  8 | 
  9 | 
 10 | class TestGitignoreParser:
 11 |     """Test class for GitignoreParser functionality."""
 12 | 
 13 |     def setup_method(self):
 14 |         """Set up test environment before each test method."""
 15 |         # Create a temporary directory for testing
 16 |         self.test_dir = tempfile.mkdtemp()
 17 |         self.repo_path = Path(self.test_dir)
 18 | 
 19 |         # Create test repository structure
 20 |         self._create_repo_structure()
 21 | 
 22 |     def teardown_method(self):
 23 |         """Clean up test environment after each test method."""
 24 |         # Remove the temporary directory
 25 |         shutil.rmtree(self.test_dir)
 26 | 
 27 |     def _create_repo_structure(self):
 28 |         """
 29 |         Create a test repository structure with multiple gitignore files.
 30 | 
 31 |         Structure:
 32 |         repo/
 33 |         ├── .gitignore
 34 |         ├── file1.txt
 35 |         ├── test.log
 36 |         ├── src/
 37 |         │   ├── .gitignore
 38 |         │   ├── main.py
 39 |         │   ├── test.log
 40 |         │   ├── build/
 41 |         │   │   └── output.o
 42 |         │   └── lib/
 43 |         │       ├── .gitignore
 44 |         │       └── cache.tmp
 45 |         └── docs/
 46 |             ├── .gitignore
 47 |             ├── api.md
 48 |             └── temp/
 49 |                 └── draft.md
 50 |         """
 51 |         # Create directories
 52 |         (self.repo_path / "src").mkdir()
 53 |         (self.repo_path / "src" / "build").mkdir()
 54 |         (self.repo_path / "src" / "lib").mkdir()
 55 |         (self.repo_path / "docs").mkdir()
 56 |         (self.repo_path / "docs" / "temp").mkdir()
 57 | 
 58 |         # Create files
 59 |         (self.repo_path / "file1.txt").touch()
 60 |         (self.repo_path / "test.log").touch()
 61 |         (self.repo_path / "src" / "main.py").touch()
 62 |         (self.repo_path / "src" / "test.log").touch()
 63 |         (self.repo_path / "src" / "build" / "output.o").touch()
 64 |         (self.repo_path / "src" / "lib" / "cache.tmp").touch()
 65 |         (self.repo_path / "docs" / "api.md").touch()
 66 |         (self.repo_path / "docs" / "temp" / "draft.md").touch()
 67 | 
 68 |         # Create root .gitignore
 69 |         root_gitignore = self.repo_path / ".gitignore"
 70 |         root_gitignore.write_text(
 71 |             """# Root gitignore
 72 | *.log
 73 | /build/
 74 | """
 75 |         )
 76 | 
 77 |         # Create src/.gitignore
 78 |         src_gitignore = self.repo_path / "src" / ".gitignore"
 79 |         src_gitignore.write_text(
 80 |             """# Source gitignore
 81 | *.o
 82 | build/
 83 | !important.log
 84 | """
 85 |         )
 86 | 
 87 |         # Create src/lib/.gitignore (deeply nested)
 88 |         src_lib_gitignore = self.repo_path / "src" / "lib" / ".gitignore"
 89 |         src_lib_gitignore.write_text(
 90 |             """# Library gitignore
 91 | *.tmp
 92 | *.cache
 93 | """
 94 |         )
 95 | 
 96 |         # Create docs/.gitignore
 97 |         docs_gitignore = self.repo_path / "docs" / ".gitignore"
 98 |         docs_gitignore.write_text(
 99 |             """# Docs gitignore
100 | temp/
101 | *.tmp
102 | """
103 |         )
104 | 
105 |     def test_initialization(self):
106 |         """Test GitignoreParser initialization."""
107 |         parser = GitignoreParser(str(self.repo_path))
108 | 
109 |         assert parser.repo_root == str(self.repo_path.absolute())
110 |         assert len(parser.get_ignore_specs()) == 4
111 | 
112 |     def test_find_gitignore_files(self):
113 |         """Test finding all gitignore files in repository, including deeply nested ones."""
114 |         parser = GitignoreParser(str(self.repo_path))
115 | 
116 |         # Get file paths from specs
117 |         gitignore_files = [spec.file_path for spec in parser.get_ignore_specs()]
118 | 
119 |         # Convert to relative paths for easier testing
120 |         rel_paths = [os.path.relpath(f, self.repo_path) for f in gitignore_files]
121 |         rel_paths.sort()
122 | 
123 |         assert len(rel_paths) == 4
124 |         assert ".gitignore" in rel_paths
125 |         assert os.path.join("src", ".gitignore") in rel_paths
126 |         assert os.path.join("src", "lib", ".gitignore") in rel_paths  # Deeply nested
127 |         assert os.path.join("docs", ".gitignore") in rel_paths
128 | 
129 |     def test_parse_patterns_root_directory(self):
130 |         """Test parsing gitignore patterns in root directory."""
131 |         # Create a simple test case with only root gitignore
132 |         test_dir = self.repo_path / "test_root"
133 |         test_dir.mkdir()
134 | 
135 |         gitignore = test_dir / ".gitignore"
136 |         gitignore.write_text(
137 |             """*.log
138 | build/
139 | /temp.txt
140 | """
141 |         )
142 | 
143 |         parser = GitignoreParser(str(test_dir))
144 |         specs = parser.get_ignore_specs()
145 | 
146 |         assert len(specs) == 1
147 |         patterns = specs[0].patterns
148 | 
149 |         assert "*.log" in patterns
150 |         assert "build/" in patterns
151 |         assert "/temp.txt" in patterns
152 | 
153 |     def test_parse_patterns_subdirectory(self):
154 |         """Test parsing gitignore patterns in subdirectory."""
155 |         # Create a test case with subdirectory gitignore
156 |         test_dir = self.repo_path / "test_sub"
157 |         test_dir.mkdir()
158 |         subdir = test_dir / "src"
159 |         subdir.mkdir()
160 | 
161 |         gitignore = subdir / ".gitignore"
162 |         gitignore.write_text(
163 |             """*.o
164 | /build/
165 | test.log
166 | """
167 |         )
168 | 
169 |         parser = GitignoreParser(str(test_dir))
170 |         specs = parser.get_ignore_specs()
171 | 
172 |         assert len(specs) == 1
173 |         patterns = specs[0].patterns
174 | 
175 |         # Non-anchored pattern should get ** prefix
176 |         assert "src/**/*.o" in patterns
177 |         # Anchored pattern should not get ** prefix
178 |         assert "src/build/" in patterns
179 |         # Non-anchored pattern without slash
180 |         assert "src/**/test.log" in patterns
181 | 
182 |     def test_should_ignore_root_patterns(self):
183 |         """Test ignoring files based on root .gitignore."""
184 |         parser = GitignoreParser(str(self.repo_path))
185 | 
186 |         # Files that should be ignored
187 |         assert parser.should_ignore("test.log")
188 |         assert parser.should_ignore(str(self.repo_path / "test.log"))
189 | 
190 |         # Files that should NOT be ignored
191 |         assert not parser.should_ignore("file1.txt")
192 |         assert not parser.should_ignore("src/main.py")
193 | 
194 |     def test_should_ignore_subdirectory_patterns(self):
195 |         """Test ignoring files based on subdirectory .gitignore files."""
196 |         parser = GitignoreParser(str(self.repo_path))
197 | 
198 |         # .o files in src should be ignored
199 |         assert parser.should_ignore("src/build/output.o")
200 | 
201 |         # build/ directory in src should be ignored
202 |         assert parser.should_ignore("src/build/")
203 | 
204 |         # temp/ directory in docs should be ignored
205 |         assert parser.should_ignore("docs/temp/draft.md")
206 | 
207 |         # But temp/ outside docs should not be ignored by docs/.gitignore
208 |         assert not parser.should_ignore("temp/file.txt")
209 | 
210 |         # Test deeply nested .gitignore in src/lib/
211 |         # .tmp files in src/lib should be ignored
212 |         assert parser.should_ignore("src/lib/cache.tmp")
213 | 
214 |         # .cache files in src/lib should also be ignored
215 |         assert parser.should_ignore("src/lib/data.cache")
216 | 
217 |         # But .tmp files outside src/lib should not be ignored by src/lib/.gitignore
218 |         assert not parser.should_ignore("src/other.tmp")
219 | 
220 |     def test_anchored_vs_non_anchored_patterns(self):
221 |         """Test the difference between anchored and non-anchored patterns."""
222 |         # Create new test structure
223 |         test_dir = self.repo_path / "test_anchored"
224 |         test_dir.mkdir()
225 |         (test_dir / "src").mkdir()
226 |         (test_dir / "src" / "subdir").mkdir()
227 |         (test_dir / "src" / "subdir" / "deep").mkdir()
228 | 
229 |         # Create src/.gitignore with both anchored and non-anchored patterns
230 |         gitignore = test_dir / "src" / ".gitignore"
231 |         gitignore.write_text(
232 |             """/temp.txt
233 | data.json
234 | """
235 |         )
236 | 
237 |         # Create test files
238 |         (test_dir / "src" / "temp.txt").touch()
239 |         (test_dir / "src" / "data.json").touch()
240 |         (test_dir / "src" / "subdir" / "temp.txt").touch()
241 |         (test_dir / "src" / "subdir" / "data.json").touch()
242 |         (test_dir / "src" / "subdir" / "deep" / "data.json").touch()
243 | 
244 |         parser = GitignoreParser(str(test_dir))
245 | 
246 |         # Anchored pattern /temp.txt should only match in src/
247 |         assert parser.should_ignore("src/temp.txt")
248 |         assert not parser.should_ignore("src/subdir/temp.txt")
249 | 
250 |         # Non-anchored pattern data.json should match anywhere under src/
251 |         assert parser.should_ignore("src/data.json")
252 |         assert parser.should_ignore("src/subdir/data.json")
253 |         assert parser.should_ignore("src/subdir/deep/data.json")
254 | 
255 |     def test_root_anchored_patterns(self):
256 |         """Test anchored patterns in root .gitignore only match root-level files."""
257 |         # Create new test structure for root anchored patterns
258 |         test_dir = self.repo_path / "test_root_anchored"
259 |         test_dir.mkdir()
260 |         (test_dir / "src").mkdir()
261 |         (test_dir / "docs").mkdir()
262 |         (test_dir / "src" / "nested").mkdir()
263 | 
264 |         # Create root .gitignore with anchored patterns
265 |         gitignore = test_dir / ".gitignore"
266 |         gitignore.write_text(
267 |             """/config.json
268 | /temp.log
269 | /build
270 | *.pyc
271 | """
272 |         )
273 | 
274 |         # Create test files at root level
275 |         (test_dir / "config.json").touch()
276 |         (test_dir / "temp.log").touch()
277 |         (test_dir / "build").mkdir()
278 |         (test_dir / "file.pyc").touch()
279 | 
280 |         # Create same-named files in subdirectories
281 |         (test_dir / "src" / "config.json").touch()
282 |         (test_dir / "src" / "temp.log").touch()
283 |         (test_dir / "src" / "build").mkdir()
284 |         (test_dir / "src" / "file.pyc").touch()
285 |         (test_dir / "docs" / "config.json").touch()
286 |         (test_dir / "docs" / "temp.log").touch()
287 |         (test_dir / "src" / "nested" / "config.json").touch()
288 |         (test_dir / "src" / "nested" / "temp.log").touch()
289 |         (test_dir / "src" / "nested" / "build").mkdir()
290 | 
291 |         parser = GitignoreParser(str(test_dir))
292 | 
293 |         # Anchored patterns should only match root-level files
294 |         assert parser.should_ignore("config.json")
295 |         assert not parser.should_ignore("src/config.json")
296 |         assert not parser.should_ignore("docs/config.json")
297 |         assert not parser.should_ignore("src/nested/config.json")
298 | 
299 |         assert parser.should_ignore("temp.log")
300 |         assert not parser.should_ignore("src/temp.log")
301 |         assert not parser.should_ignore("docs/temp.log")
302 |         assert not parser.should_ignore("src/nested/temp.log")
303 | 
304 |         assert parser.should_ignore("build")
305 |         assert not parser.should_ignore("src/build")
306 |         assert not parser.should_ignore("src/nested/build")
307 | 
308 |         # Non-anchored patterns should match everywhere
309 |         assert parser.should_ignore("file.pyc")
310 |         assert parser.should_ignore("src/file.pyc")
311 | 
312 |     def test_mixed_anchored_and_non_anchored_root_patterns(self):
313 |         """Test mix of anchored and non-anchored patterns in root .gitignore."""
314 |         test_dir = self.repo_path / "test_mixed_patterns"
315 |         test_dir.mkdir()
316 |         (test_dir / "app").mkdir()
317 |         (test_dir / "tests").mkdir()
318 |         (test_dir / "app" / "modules").mkdir()
319 | 
320 |         # Create root .gitignore with mixed patterns
321 |         gitignore = test_dir / ".gitignore"
322 |         gitignore.write_text(
323 |             """/secrets.env
324 | /dist/
325 | node_modules/
326 | *.tmp
327 | /app/local.config
328 | debug.log
329 | """
330 |         )
331 | 
332 |         # Create test files and directories
333 |         (test_dir / "secrets.env").touch()
334 |         (test_dir / "dist").mkdir()
335 |         (test_dir / "node_modules").mkdir()
336 |         (test_dir / "file.tmp").touch()
337 |         (test_dir / "app" / "local.config").touch()
338 |         (test_dir / "debug.log").touch()
339 | 
340 |         # Create same files in subdirectories
341 |         (test_dir / "app" / "secrets.env").touch()
342 |         (test_dir / "app" / "dist").mkdir()
343 |         (test_dir / "app" / "node_modules").mkdir()
344 |         (test_dir / "app" / "file.tmp").touch()
345 |         (test_dir / "app" / "debug.log").touch()
346 |         (test_dir / "tests" / "secrets.env").touch()
347 |         (test_dir / "tests" / "node_modules").mkdir()
348 |         (test_dir / "tests" / "debug.log").touch()
349 |         (test_dir / "app" / "modules" / "local.config").touch()
350 | 
351 |         parser = GitignoreParser(str(test_dir))
352 | 
353 |         # Anchored patterns should only match at root
354 |         assert parser.should_ignore("secrets.env")
355 |         assert not parser.should_ignore("app/secrets.env")
356 |         assert not parser.should_ignore("tests/secrets.env")
357 | 
358 |         assert parser.should_ignore("dist")
359 |         assert not parser.should_ignore("app/dist")
360 | 
361 |         assert parser.should_ignore("app/local.config")
362 |         assert not parser.should_ignore("app/modules/local.config")
363 | 
364 |         # Non-anchored patterns should match everywhere
365 |         assert parser.should_ignore("node_modules")
366 |         assert parser.should_ignore("app/node_modules")
367 |         assert parser.should_ignore("tests/node_modules")
368 | 
369 |         assert parser.should_ignore("file.tmp")
370 |         assert parser.should_ignore("app/file.tmp")
371 | 
372 |         assert parser.should_ignore("debug.log")
373 |         assert parser.should_ignore("app/debug.log")
374 |         assert parser.should_ignore("tests/debug.log")
375 | 
376 |     def test_negation_patterns(self):
377 |         """Test negation patterns are parsed correctly."""
378 |         test_dir = self.repo_path / "test_negation"
379 |         test_dir.mkdir()
380 | 
381 |         gitignore = test_dir / ".gitignore"
382 |         gitignore.write_text(
383 |             """*.log
384 | !important.log
385 | !src/keep.log
386 | """
387 |         )
388 | 
389 |         parser = GitignoreParser(str(test_dir))
390 |         specs = parser.get_ignore_specs()
391 | 
392 |         assert len(specs) == 1
393 |         patterns = specs[0].patterns
394 | 
395 |         assert "*.log" in patterns
396 |         assert "!important.log" in patterns
397 |         assert "!src/keep.log" in patterns
398 | 
399 |     def test_comments_and_empty_lines(self):
400 |         """Test that comments and empty lines are ignored."""
401 |         test_dir = self.repo_path / "test_comments"
402 |         test_dir.mkdir()
403 | 
404 |         gitignore = test_dir / ".gitignore"
405 |         gitignore.write_text(
406 |             """# This is a comment
407 | *.log
408 | 
409 | # Another comment
410 |   # Indented comment
411 | 
412 | build/
413 | """
414 |         )
415 | 
416 |         parser = GitignoreParser(str(test_dir))
417 |         specs = parser.get_ignore_specs()
418 | 
419 |         assert len(specs) == 1
420 |         patterns = specs[0].patterns
421 | 
422 |         assert len(patterns) == 2
423 |         assert "*.log" in patterns
424 |         assert "build/" in patterns
425 | 
426 |     def test_escaped_characters(self):
427 |         """Test escaped special characters."""
428 |         test_dir = self.repo_path / "test_escaped"
429 |         test_dir.mkdir()
430 | 
431 |         gitignore = test_dir / ".gitignore"
432 |         gitignore.write_text(
433 |             """\\#not-a-comment.txt
434 | \\!not-negation.txt
435 | """
436 |         )
437 | 
438 |         parser = GitignoreParser(str(test_dir))
439 |         specs = parser.get_ignore_specs()
440 | 
441 |         assert len(specs) == 1
442 |         patterns = specs[0].patterns
443 | 
444 |         assert "#not-a-comment.txt" in patterns
445 |         assert "!not-negation.txt" in patterns
446 | 
447 |     def test_escaped_negation_patterns(self):
448 |         test_dir = self.repo_path / "test_escaped_negation"
449 |         test_dir.mkdir()
450 | 
451 |         gitignore = test_dir / ".gitignore"
452 |         gitignore.write_text(
453 |             """*.log
454 | \\!not-negation.log
455 | !actual-negation.log
456 | """
457 |         )
458 | 
459 |         parser = GitignoreParser(str(test_dir))
460 |         specs = parser.get_ignore_specs()
461 | 
462 |         assert len(specs) == 1
463 |         patterns = specs[0].patterns
464 | 
465 |         # Key assertions: escaped exclamation becomes literal, real negation preserved
466 |         assert "!not-negation.log" in patterns  # escaped -> literal
467 |         assert "!actual-negation.log" in patterns  # real negation preserved
468 | 
469 |         # Test the actual behavioral difference between escaped and real negation:
470 |         # *.log pattern should ignore test.log
471 |         assert parser.should_ignore("test.log")
472 | 
473 |         # Escaped negation file should still be ignored by *.log pattern
474 |         assert parser.should_ignore("!not-negation.log")
475 | 
476 |         # Actual negation should override the *.log pattern
477 |         assert not parser.should_ignore("actual-negation.log")
478 | 
479 |     def test_glob_patterns(self):
480 |         """Test various glob patterns work correctly."""
481 |         test_dir = self.repo_path / "test_glob"
482 |         test_dir.mkdir()
483 | 
484 |         gitignore = test_dir / ".gitignore"
485 |         gitignore.write_text(
486 |             """*.pyc
487 | **/*.tmp
488 | src/*.o
489 | !src/important.o
490 | [Tt]est*
491 | """
492 |         )
493 | 
494 |         # Create test files
495 |         (test_dir / "src").mkdir()
496 |         (test_dir / "src" / "nested").mkdir()
497 |         (test_dir / "file.pyc").touch()
498 |         (test_dir / "src" / "file.pyc").touch()
499 |         (test_dir / "file.tmp").touch()
500 |         (test_dir / "src" / "nested" / "file.tmp").touch()
501 |         (test_dir / "src" / "file.o").touch()
502 |         (test_dir / "src" / "important.o").touch()
503 |         (test_dir / "Test.txt").touch()
504 |         (test_dir / "test.log").touch()
505 | 
506 |         parser = GitignoreParser(str(test_dir))
507 | 
508 |         # *.pyc should match everywhere
509 |         assert parser.should_ignore("file.pyc")
510 |         assert parser.should_ignore("src/file.pyc")
511 | 
512 |         # **/*.tmp should match all .tmp files
513 |         assert parser.should_ignore("file.tmp")
514 |         assert parser.should_ignore("src/nested/file.tmp")
515 | 
516 |         # src/*.o should only match .o files directly in src/
517 |         assert parser.should_ignore("src/file.o")
518 | 
519 |         # Character class patterns
520 |         assert parser.should_ignore("Test.txt")
521 |         assert parser.should_ignore("test.log")
522 | 
523 |     def test_empty_gitignore(self):
524 |         """Test handling of empty gitignore files."""
525 |         test_dir = self.repo_path / "test_empty"
526 |         test_dir.mkdir()
527 | 
528 |         gitignore = test_dir / ".gitignore"
529 |         gitignore.write_text("")
530 | 
531 |         parser = GitignoreParser(str(test_dir))
532 | 
533 |         # Should not crash and should return empty list
534 |         assert len(parser.get_ignore_specs()) == 0
535 | 
536 |     def test_malformed_gitignore(self):
537 |         """Test handling of malformed gitignore content."""
538 |         test_dir = self.repo_path / "test_malformed"
539 |         test_dir.mkdir()
540 | 
541 |         gitignore = test_dir / ".gitignore"
542 |         gitignore.write_text(
543 |             """# Only comments and empty lines
544 |     
545 | # More comments
546 |     
547 |     """
548 |         )
549 | 
550 |         parser = GitignoreParser(str(test_dir))
551 | 
552 |         # Should handle gracefully
553 |         assert len(parser.get_ignore_specs()) == 0
554 | 
555 |     def test_reload(self):
556 |         """Test reloading gitignore files."""
557 |         test_dir = self.repo_path / "test_reload"
558 |         test_dir.mkdir()
559 | 
560 |         # Create initial gitignore
561 |         gitignore = test_dir / ".gitignore"
562 |         gitignore.write_text("*.log")
563 | 
564 |         parser = GitignoreParser(str(test_dir))
565 |         assert len(parser.get_ignore_specs()) == 1
566 |         assert parser.should_ignore("test.log")
567 | 
568 |         # Modify gitignore
569 |         gitignore.write_text("*.tmp")
570 | 
571 |         # Without reload, should still use old patterns
572 |         assert parser.should_ignore("test.log")
573 |         assert not parser.should_ignore("test.tmp")
574 | 
575 |         # After reload, should use new patterns
576 |         parser.reload()
577 |         assert not parser.should_ignore("test.log")
578 |         assert parser.should_ignore("test.tmp")
579 | 
580 |     def test_gitignore_spec_matches(self):
581 |         """Test GitignoreSpec.matches method."""
582 |         spec = GitignoreSpec("/path/to/.gitignore", ["*.log", "build/", "!important.log"])
583 | 
584 |         assert spec.matches("test.log")
585 |         assert spec.matches("build/output.o")
586 |         assert spec.matches("src/test.log")
587 | 
588 |         # Note: Negation patterns in pathspec work differently than in git
589 |         # This is a limitation of the pathspec library
590 | 
591 |     def test_subdirectory_gitignore_pattern_scoping(self):
592 |         """Test that subdirectory .gitignore patterns are scoped correctly."""
593 |         # Create test structure: foo/ with subdirectory bar/
594 |         test_dir = self.repo_path / "test_subdir_scoping"
595 |         test_dir.mkdir()
596 |         (test_dir / "foo").mkdir()
597 |         (test_dir / "foo" / "bar").mkdir()
598 | 
599 |         # Create files in various locations
600 |         (test_dir / "foo.txt").touch()  # root level
601 |         (test_dir / "foo" / "foo.txt").touch()  # in foo/
602 |         (test_dir / "foo" / "bar" / "foo.txt").touch()  # in foo/bar/
603 | 
604 |         # Test case 1: foo.txt in foo/.gitignore should only ignore in foo/ subtree
605 |         gitignore = test_dir / "foo" / ".gitignore"
606 |         gitignore.write_text("foo.txt\n")
607 | 
608 |         parser = GitignoreParser(str(test_dir))
609 | 
610 |         # foo.txt at root should NOT be ignored by foo/.gitignore
611 |         assert not parser.should_ignore("foo.txt"), "Root foo.txt should not be ignored by foo/.gitignore"
612 | 
613 |         # foo.txt in foo/ should be ignored
614 |         assert parser.should_ignore("foo/foo.txt"), "foo/foo.txt should be ignored"
615 | 
616 |         # foo.txt in foo/bar/ should be ignored (within foo/ subtree)
617 |         assert parser.should_ignore("foo/bar/foo.txt"), "foo/bar/foo.txt should be ignored"
618 | 
619 |     def test_anchored_pattern_in_subdirectory(self):
620 |         """Test that anchored patterns in subdirectory only match immediate children."""
621 |         test_dir = self.repo_path / "test_anchored_subdir"
622 |         test_dir.mkdir()
623 |         (test_dir / "foo").mkdir()
624 |         (test_dir / "foo" / "bar").mkdir()
625 | 
626 |         # Create files
627 |         (test_dir / "foo.txt").touch()  # root level
628 |         (test_dir / "foo" / "foo.txt").touch()  # in foo/
629 |         (test_dir / "foo" / "bar" / "foo.txt").touch()  # in foo/bar/
630 | 
631 |         # Test case 2: /foo.txt in foo/.gitignore should only match foo/foo.txt
632 |         gitignore = test_dir / "foo" / ".gitignore"
633 |         gitignore.write_text("/foo.txt\n")
634 | 
635 |         parser = GitignoreParser(str(test_dir))
636 | 
637 |         # foo.txt at root should NOT be ignored
638 |         assert not parser.should_ignore("foo.txt"), "Root foo.txt should not be ignored"
639 | 
640 |         # foo.txt directly in foo/ should be ignored
641 |         assert parser.should_ignore("foo/foo.txt"), "foo/foo.txt should be ignored by /foo.txt pattern"
642 | 
643 |         # foo.txt in foo/bar/ should NOT be ignored (anchored pattern only matches immediate children)
644 |         assert not parser.should_ignore("foo/bar/foo.txt"), "foo/bar/foo.txt should NOT be ignored by /foo.txt pattern"
645 | 
646 |     def test_double_star_pattern_scoping(self):
647 |         """Test that **/pattern in subdirectory only applies within that subtree."""
648 |         test_dir = self.repo_path / "test_doublestar_scope"
649 |         test_dir.mkdir()
650 |         (test_dir / "foo").mkdir()
651 |         (test_dir / "foo" / "bar").mkdir()
652 |         (test_dir / "other").mkdir()
653 | 
654 |         # Create files
655 |         (test_dir / "foo.txt").touch()  # root level
656 |         (test_dir / "foo" / "foo.txt").touch()  # in foo/
657 |         (test_dir / "foo" / "bar" / "foo.txt").touch()  # in foo/bar/
658 |         (test_dir / "other" / "foo.txt").touch()  # in other/
659 | 
660 |         # Test case 3: **/foo.txt in foo/.gitignore should only ignore within foo/ subtree
661 |         gitignore = test_dir / "foo" / ".gitignore"
662 |         gitignore.write_text("**/foo.txt\n")
663 | 
664 |         parser = GitignoreParser(str(test_dir))
665 | 
666 |         # foo.txt at root should NOT be ignored
667 |         assert not parser.should_ignore("foo.txt"), "Root foo.txt should not be ignored by foo/.gitignore"
668 | 
669 |         # foo.txt in foo/ should be ignored
670 |         assert parser.should_ignore("foo/foo.txt"), "foo/foo.txt should be ignored"
671 | 
672 |         # foo.txt in foo/bar/ should be ignored (within foo/ subtree)
673 |         assert parser.should_ignore("foo/bar/foo.txt"), "foo/bar/foo.txt should be ignored"
674 | 
675 |         # foo.txt in other/ should NOT be ignored (outside foo/ subtree)
676 |         assert not parser.should_ignore("other/foo.txt"), "other/foo.txt should NOT be ignored by foo/.gitignore"
677 | 
678 |     def test_anchored_double_star_pattern(self):
679 |         """Test that /**/pattern in subdirectory works correctly."""
680 |         test_dir = self.repo_path / "test_anchored_doublestar"
681 |         test_dir.mkdir()
682 |         (test_dir / "foo").mkdir()
683 |         (test_dir / "foo" / "bar").mkdir()
684 |         (test_dir / "other").mkdir()
685 | 
686 |         # Create files
687 |         (test_dir / "foo.txt").touch()  # root level
688 |         (test_dir / "foo" / "foo.txt").touch()  # in foo/
689 |         (test_dir / "foo" / "bar" / "foo.txt").touch()  # in foo/bar/
690 |         (test_dir / "other" / "foo.txt").touch()  # in other/
691 | 
692 |         # Test case 4: /**/foo.txt in foo/.gitignore should correctly ignore only within foo/ subtree
693 |         gitignore = test_dir / "foo" / ".gitignore"
694 |         gitignore.write_text("/**/foo.txt\n")
695 | 
696 |         parser = GitignoreParser(str(test_dir))
697 | 
698 |         # foo.txt at root should NOT be ignored
699 |         assert not parser.should_ignore("foo.txt"), "Root foo.txt should not be ignored"
700 | 
701 |         # foo.txt in foo/ should be ignored
702 |         assert parser.should_ignore("foo/foo.txt"), "foo/foo.txt should be ignored"
703 | 
704 |         # foo.txt in foo/bar/ should be ignored (within foo/ subtree)
705 |         assert parser.should_ignore("foo/bar/foo.txt"), "foo/bar/foo.txt should be ignored"
706 | 
707 |         # foo.txt in other/ should NOT be ignored (outside foo/ subtree)
708 |         assert not parser.should_ignore("other/foo.txt"), "other/foo.txt should NOT be ignored by foo/.gitignore"
709 | 
```
Page 9/14FirstPrevNextLast