#
tokens: 32118/50000 1/410 files (page 20/21)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 20 of 21. Use http://codebase.md/oraios/serena?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── issue--bug--performance-problem--question-.md
│   └── workflows
│       ├── codespell.yml
│       ├── docker.yml
│       ├── docs.yaml
│       ├── junie.yml
│       ├── publish.yml
│       └── pytest.yml
├── .gitignore
├── .serena
│   ├── .gitignore
│   ├── memories
│   │   ├── adding_new_language_support_guide.md
│   │   ├── serena_core_concepts_and_architecture.md
│   │   ├── serena_repository_structure.md
│   │   └── suggested_commands.md
│   └── project.yml
├── .vscode
│   └── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── compose.yaml
├── CONTRIBUTING.md
├── docker_build_and_run.sh
├── DOCKER.md
├── Dockerfile
├── docs
│   ├── _config.yml
│   ├── _static
│   │   └── images
│   │       └── jetbrains-marketplace-button.png
│   ├── .gitignore
│   ├── 01-about
│   │   ├── 000_intro.md
│   │   ├── 010_llm-integration.md
│   │   ├── 020_programming-languages.md
│   │   ├── 030_serena-in-action.md
│   │   ├── 035_tools.md
│   │   ├── 040_comparison-to-other-agents.md
│   │   └── 050_acknowledgements.md
│   ├── 02-usage
│   │   ├── 000_intro.md
│   │   ├── 010_prerequisites.md
│   │   ├── 020_running.md
│   │   ├── 025_jetbrains_plugin.md
│   │   ├── 030_clients.md
│   │   ├── 040_workflow.md
│   │   ├── 050_configuration.md
│   │   ├── 060_dashboard.md
│   │   ├── 070_security.md
│   │   └── 999_additional-usage.md
│   ├── 03-special-guides
│   │   ├── 000_intro.md
│   │   ├── custom_agent.md
│   │   ├── groovy_setup_guide_for_serena.md
│   │   ├── scala_setup_guide_for_serena.md
│   │   └── serena_on_chatgpt.md
│   ├── autogen_rst.py
│   ├── create_toc.py
│   └── index.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── pyproject.toml
├── README.md
├── repo_dir_sync.py
├── resources
│   ├── jetbrains-marketplace-button.cdr
│   ├── serena-icons.cdr
│   ├── serena-logo-dark-mode.svg
│   ├── serena-logo.cdr
│   ├── serena-logo.svg
│   └── vscode_sponsor_logo.png
├── roadmap.md
├── scripts
│   ├── agno_agent.py
│   ├── demo_run_tools.py
│   ├── gen_prompt_factory.py
│   ├── mcp_server.py
│   ├── print_mode_context_options.py
│   ├── print_tool_overview.py
│   └── profile_tool_call.py
├── src
│   ├── interprompt
│   │   ├── __init__.py
│   │   ├── .syncCommitId.remote
│   │   ├── .syncCommitId.this
│   │   ├── jinja_template.py
│   │   ├── multilang_prompt.py
│   │   ├── prompt_factory.py
│   │   └── util
│   │       ├── __init__.py
│   │       └── class_decorators.py
│   ├── README.md
│   ├── serena
│   │   ├── __init__.py
│   │   ├── agent.py
│   │   ├── agno.py
│   │   ├── analytics.py
│   │   ├── cli.py
│   │   ├── code_editor.py
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   ├── context_mode.py
│   │   │   └── serena_config.py
│   │   ├── constants.py
│   │   ├── dashboard.py
│   │   ├── generated
│   │   │   └── generated_prompt_factory.py
│   │   ├── gui_log_viewer.py
│   │   ├── ls_manager.py
│   │   ├── mcp.py
│   │   ├── project.py
│   │   ├── prompt_factory.py
│   │   ├── resources
│   │   │   ├── config
│   │   │   │   ├── contexts
│   │   │   │   │   ├── agent.yml
│   │   │   │   │   ├── chatgpt.yml
│   │   │   │   │   ├── claude-code.yml
│   │   │   │   │   ├── codex.yml
│   │   │   │   │   ├── context.template.yml
│   │   │   │   │   ├── desktop-app.yml
│   │   │   │   │   ├── ide.yml
│   │   │   │   │   └── oaicompat-agent.yml
│   │   │   │   ├── internal_modes
│   │   │   │   │   └── jetbrains.yml
│   │   │   │   ├── modes
│   │   │   │   │   ├── editing.yml
│   │   │   │   │   ├── interactive.yml
│   │   │   │   │   ├── mode.template.yml
│   │   │   │   │   ├── no-memories.yml
│   │   │   │   │   ├── no-onboarding.yml
│   │   │   │   │   ├── onboarding.yml
│   │   │   │   │   ├── one-shot.yml
│   │   │   │   │   └── planning.yml
│   │   │   │   └── prompt_templates
│   │   │   │       ├── simple_tool_outputs.yml
│   │   │   │       └── system_prompt.yml
│   │   │   ├── dashboard
│   │   │   │   ├── dashboard.css
│   │   │   │   ├── dashboard.js
│   │   │   │   ├── index.html
│   │   │   │   ├── jquery.min.js
│   │   │   │   ├── serena-icon-16.png
│   │   │   │   ├── serena-icon-32.png
│   │   │   │   ├── serena-icon-48.png
│   │   │   │   ├── serena-logo-dark-mode.svg
│   │   │   │   ├── serena-logo.svg
│   │   │   │   ├── serena-logs-dark-mode.png
│   │   │   │   └── serena-logs.png
│   │   │   ├── project.template.yml
│   │   │   └── serena_config.template.yml
│   │   ├── symbol.py
│   │   ├── task_executor.py
│   │   ├── text_utils.py
│   │   ├── tools
│   │   │   ├── __init__.py
│   │   │   ├── cmd_tools.py
│   │   │   ├── config_tools.py
│   │   │   ├── file_tools.py
│   │   │   ├── jetbrains_plugin_client.py
│   │   │   ├── jetbrains_tools.py
│   │   │   ├── memory_tools.py
│   │   │   ├── symbol_tools.py
│   │   │   ├── tools_base.py
│   │   │   └── workflow_tools.py
│   │   └── util
│   │       ├── class_decorators.py
│   │       ├── cli_util.py
│   │       ├── exception.py
│   │       ├── file_system.py
│   │       ├── general.py
│   │       ├── git.py
│   │       ├── gui.py
│   │       ├── inspection.py
│   │       ├── logging.py
│   │       ├── shell.py
│   │       └── thread.py
│   └── solidlsp
│       ├── __init__.py
│       ├── .gitignore
│       ├── language_servers
│       │   ├── al_language_server.py
│       │   ├── bash_language_server.py
│       │   ├── clangd_language_server.py
│       │   ├── clojure_lsp.py
│       │   ├── common.py
│       │   ├── csharp_language_server.py
│       │   ├── dart_language_server.py
│       │   ├── eclipse_jdtls.py
│       │   ├── elixir_tools
│       │   │   ├── __init__.py
│       │   │   ├── elixir_tools.py
│       │   │   └── README.md
│       │   ├── elm_language_server.py
│       │   ├── erlang_language_server.py
│       │   ├── fortran_language_server.py
│       │   ├── fsharp_language_server.py
│       │   ├── gopls.py
│       │   ├── groovy_language_server.py
│       │   ├── haskell_language_server.py
│       │   ├── intelephense.py
│       │   ├── jedi_server.py
│       │   ├── julia_server.py
│       │   ├── kotlin_language_server.py
│       │   ├── lua_ls.py
│       │   ├── marksman.py
│       │   ├── matlab_language_server.py
│       │   ├── nixd_ls.py
│       │   ├── omnisharp
│       │   │   ├── initialize_params.json
│       │   │   ├── runtime_dependencies.json
│       │   │   └── workspace_did_change_configuration.json
│       │   ├── omnisharp.py
│       │   ├── pascal_server.py
│       │   ├── perl_language_server.py
│       │   ├── powershell_language_server.py
│       │   ├── pyright_server.py
│       │   ├── r_language_server.py
│       │   ├── regal_server.py
│       │   ├── ruby_lsp.py
│       │   ├── rust_analyzer.py
│       │   ├── scala_language_server.py
│       │   ├── solargraph.py
│       │   ├── sourcekit_lsp.py
│       │   ├── taplo_server.py
│       │   ├── terraform_ls.py
│       │   ├── typescript_language_server.py
│       │   ├── vts_language_server.py
│       │   ├── vue_language_server.py
│       │   ├── yaml_language_server.py
│       │   └── zls.py
│       ├── ls_config.py
│       ├── ls_exceptions.py
│       ├── ls_handler.py
│       ├── ls_request.py
│       ├── ls_types.py
│       ├── ls_utils.py
│       ├── ls.py
│       ├── lsp_protocol_handler
│       │   ├── lsp_constants.py
│       │   ├── lsp_requests.py
│       │   ├── lsp_types.py
│       │   └── server.py
│       ├── settings.py
│       └── util
│           ├── cache.py
│           ├── subprocess_util.py
│           └── zip.py
├── sync.py
├── test
│   ├── __init__.py
│   ├── conftest.py
│   ├── resources
│   │   └── repos
│   │       ├── al
│   │       │   └── test_repo
│   │       │       ├── app.json
│   │       │       └── src
│   │       │           ├── Codeunits
│   │       │           │   ├── CustomerMgt.Codeunit.al
│   │       │           │   └── PaymentProcessorImpl.Codeunit.al
│   │       │           ├── Enums
│   │       │           │   └── CustomerType.Enum.al
│   │       │           ├── Interfaces
│   │       │           │   └── IPaymentProcessor.Interface.al
│   │       │           ├── Pages
│   │       │           │   ├── CustomerCard.Page.al
│   │       │           │   └── CustomerList.Page.al
│   │       │           ├── TableExtensions
│   │       │           │   └── Item.TableExt.al
│   │       │           └── Tables
│   │       │               └── Customer.Table.al
│   │       ├── bash
│   │       │   └── test_repo
│   │       │       ├── config.sh
│   │       │       ├── main.sh
│   │       │       └── utils.sh
│   │       ├── clojure
│   │       │   └── test_repo
│   │       │       ├── deps.edn
│   │       │       └── src
│   │       │           └── test_app
│   │       │               ├── core.clj
│   │       │               └── utils.clj
│   │       ├── csharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Models
│   │       │       │   └── Person.cs
│   │       │       ├── Program.cs
│   │       │       ├── serena.sln
│   │       │       └── TestProject.csproj
│   │       ├── dart
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── helper.dart
│   │       │       │   ├── main.dart
│   │       │       │   └── models.dart
│   │       │       └── pubspec.yaml
│   │       ├── elixir
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── examples.ex
│   │       │       │   ├── ignored_dir
│   │       │       │   │   └── ignored_module.ex
│   │       │       │   ├── models.ex
│   │       │       │   ├── services.ex
│   │       │       │   ├── test_repo.ex
│   │       │       │   └── utils.ex
│   │       │       ├── mix.exs
│   │       │       ├── mix.lock
│   │       │       ├── scripts
│   │       │       │   └── build_script.ex
│   │       │       └── test
│   │       │           ├── models_test.exs
│   │       │           └── test_repo_test.exs
│   │       ├── elm
│   │       │   └── test_repo
│   │       │       ├── elm.json
│   │       │       ├── Main.elm
│   │       │       └── Utils.elm
│   │       ├── erlang
│   │       │   └── test_repo
│   │       │       ├── hello.erl
│   │       │       ├── ignored_dir
│   │       │       │   └── ignored_module.erl
│   │       │       ├── include
│   │       │       │   ├── records.hrl
│   │       │       │   └── types.hrl
│   │       │       ├── math_utils.erl
│   │       │       ├── rebar.config
│   │       │       ├── src
│   │       │       │   ├── app.erl
│   │       │       │   ├── models.erl
│   │       │       │   ├── services.erl
│   │       │       │   └── utils.erl
│   │       │       └── test
│   │       │           ├── models_tests.erl
│   │       │           └── utils_tests.erl
│   │       ├── fortran
│   │       │   └── test_repo
│   │       │       ├── main.f90
│   │       │       └── modules
│   │       │           ├── geometry.f90
│   │       │           └── math_utils.f90
│   │       ├── fsharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Calculator.fs
│   │       │       ├── Models
│   │       │       │   └── Person.fs
│   │       │       ├── Program.fs
│   │       │       ├── README.md
│   │       │       └── TestProject.fsproj
│   │       ├── go
│   │       │   └── test_repo
│   │       │       └── main.go
│   │       ├── groovy
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle
│   │       │       └── src
│   │       │           └── main
│   │       │               └── groovy
│   │       │                   └── com
│   │       │                       └── example
│   │       │                           ├── Main.groovy
│   │       │                           ├── Model.groovy
│   │       │                           ├── ModelUser.groovy
│   │       │                           └── Utils.groovy
│   │       ├── haskell
│   │       │   └── test_repo
│   │       │       ├── app
│   │       │       │   └── Main.hs
│   │       │       ├── haskell-test-repo.cabal
│   │       │       ├── package.yaml
│   │       │       ├── src
│   │       │       │   ├── Calculator.hs
│   │       │       │   └── Helper.hs
│   │       │       └── stack.yaml
│   │       ├── java
│   │       │   └── test_repo
│   │       │       ├── pom.xml
│   │       │       └── src
│   │       │           └── main
│   │       │               └── java
│   │       │                   └── test_repo
│   │       │                       ├── Main.java
│   │       │                       ├── Model.java
│   │       │                       ├── ModelUser.java
│   │       │                       └── Utils.java
│   │       ├── julia
│   │       │   └── test_repo
│   │       │       ├── lib
│   │       │       │   └── helper.jl
│   │       │       └── main.jl
│   │       ├── kotlin
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle.kts
│   │       │       └── src
│   │       │           └── main
│   │       │               └── kotlin
│   │       │                   └── test_repo
│   │       │                       ├── Main.kt
│   │       │                       ├── Model.kt
│   │       │                       ├── ModelUser.kt
│   │       │                       └── Utils.kt
│   │       ├── lua
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── main.lua
│   │       │       ├── src
│   │       │       │   ├── calculator.lua
│   │       │       │   └── utils.lua
│   │       │       └── tests
│   │       │           └── test_calculator.lua
│   │       ├── markdown
│   │       │   └── test_repo
│   │       │       ├── api.md
│   │       │       ├── CONTRIBUTING.md
│   │       │       ├── guide.md
│   │       │       └── README.md
│   │       ├── matlab
│   │       │   └── test_repo
│   │       │       ├── Calculator.m
│   │       │       └── main.m
│   │       ├── nix
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── default.nix
│   │       │       ├── flake.nix
│   │       │       ├── lib
│   │       │       │   └── utils.nix
│   │       │       ├── modules
│   │       │       │   └── example.nix
│   │       │       └── scripts
│   │       │           └── hello.sh
│   │       ├── pascal
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   └── helper.pas
│   │       │       └── main.pas
│   │       ├── perl
│   │       │   └── test_repo
│   │       │       ├── helper.pl
│   │       │       └── main.pl
│   │       ├── php
│   │       │   └── test_repo
│   │       │       ├── helper.php
│   │       │       ├── index.php
│   │       │       └── simple_var.php
│   │       ├── powershell
│   │       │   └── test_repo
│   │       │       ├── main.ps1
│   │       │       ├── PowerShellEditorServices.json
│   │       │       └── utils.ps1
│   │       ├── python
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── custom_test
│   │       │       │   ├── __init__.py
│   │       │       │   └── advanced_features.py
│   │       │       ├── examples
│   │       │       │   ├── __init__.py
│   │       │       │   └── user_management.py
│   │       │       ├── ignore_this_dir_with_postfix
│   │       │       │   └── ignored_module.py
│   │       │       ├── scripts
│   │       │       │   ├── __init__.py
│   │       │       │   └── run_app.py
│   │       │       └── test_repo
│   │       │           ├── __init__.py
│   │       │           ├── complex_types.py
│   │       │           ├── models.py
│   │       │           ├── name_collisions.py
│   │       │           ├── nested_base.py
│   │       │           ├── nested.py
│   │       │           ├── overloaded.py
│   │       │           ├── services.py
│   │       │           ├── utils.py
│   │       │           └── variables.py
│   │       ├── r
│   │       │   └── test_repo
│   │       │       ├── .Rbuildignore
│   │       │       ├── DESCRIPTION
│   │       │       ├── examples
│   │       │       │   └── analysis.R
│   │       │       ├── NAMESPACE
│   │       │       └── R
│   │       │           ├── models.R
│   │       │           └── utils.R
│   │       ├── rego
│   │       │   └── test_repo
│   │       │       ├── policies
│   │       │       │   ├── authz.rego
│   │       │       │   └── validation.rego
│   │       │       └── utils
│   │       │           └── helpers.rego
│   │       ├── ruby
│   │       │   └── test_repo
│   │       │       ├── .solargraph.yml
│   │       │       ├── examples
│   │       │       │   └── user_management.rb
│   │       │       ├── lib.rb
│   │       │       ├── main.rb
│   │       │       ├── models.rb
│   │       │       ├── nested.rb
│   │       │       ├── services.rb
│   │       │       └── variables.rb
│   │       ├── rust
│   │       │   ├── test_repo
│   │       │   │   ├── Cargo.lock
│   │       │   │   ├── Cargo.toml
│   │       │   │   └── src
│   │       │   │       ├── lib.rs
│   │       │   │       └── main.rs
│   │       │   └── test_repo_2024
│   │       │       ├── Cargo.lock
│   │       │       ├── Cargo.toml
│   │       │       └── src
│   │       │           ├── lib.rs
│   │       │           └── main.rs
│   │       ├── scala
│   │       │   ├── build.sbt
│   │       │   ├── project
│   │       │   │   ├── build.properties
│   │       │   │   ├── metals.sbt
│   │       │   │   └── plugins.sbt
│   │       │   └── src
│   │       │       └── main
│   │       │           └── scala
│   │       │               └── com
│   │       │                   └── example
│   │       │                       ├── Main.scala
│   │       │                       └── Utils.scala
│   │       ├── swift
│   │       │   └── test_repo
│   │       │       ├── Package.swift
│   │       │       └── src
│   │       │           ├── main.swift
│   │       │           └── utils.swift
│   │       ├── terraform
│   │       │   └── test_repo
│   │       │       ├── data.tf
│   │       │       ├── main.tf
│   │       │       ├── outputs.tf
│   │       │       └── variables.tf
│   │       ├── toml
│   │       │   └── test_repo
│   │       │       ├── Cargo.toml
│   │       │       ├── config.toml
│   │       │       └── pyproject.toml
│   │       ├── typescript
│   │       │   └── test_repo
│   │       │       ├── .serena
│   │       │       │   └── project.yml
│   │       │       ├── index.ts
│   │       │       ├── tsconfig.json
│   │       │       ├── use_helper.ts
│   │       │       └── ws_manager.js
│   │       ├── vue
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── index.html
│   │       │       ├── package.json
│   │       │       ├── src
│   │       │       │   ├── App.vue
│   │       │       │   ├── components
│   │       │       │   │   ├── CalculatorButton.vue
│   │       │       │   │   ├── CalculatorDisplay.vue
│   │       │       │   │   └── CalculatorInput.vue
│   │       │       │   ├── composables
│   │       │       │   │   ├── useFormatter.ts
│   │       │       │   │   └── useTheme.ts
│   │       │       │   ├── main.ts
│   │       │       │   ├── stores
│   │       │       │   │   └── calculator.ts
│   │       │       │   └── types
│   │       │       │       └── index.ts
│   │       │       ├── tsconfig.json
│   │       │       ├── tsconfig.node.json
│   │       │       └── vite.config.ts
│   │       ├── yaml
│   │       │   └── test_repo
│   │       │       ├── config.yaml
│   │       │       ├── data.yaml
│   │       │       └── services.yml
│   │       └── zig
│   │           └── test_repo
│   │               ├── .gitignore
│   │               ├── build.zig
│   │               ├── src
│   │               │   ├── calculator.zig
│   │               │   ├── main.zig
│   │               │   └── math_utils.zig
│   │               └── zls.json
│   ├── serena
│   │   ├── __init__.py
│   │   ├── __snapshots__
│   │   │   └── test_symbol_editing.ambr
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   └── test_serena_config.py
│   │   ├── test_cli_project_commands.py
│   │   ├── test_edit_marker.py
│   │   ├── test_mcp.py
│   │   ├── test_serena_agent.py
│   │   ├── test_symbol_editing.py
│   │   ├── test_symbol.py
│   │   ├── test_task_executor.py
│   │   ├── test_text_utils.py
│   │   ├── test_tool_parameter_types.py
│   │   └── util
│   │       ├── test_exception.py
│   │       └── test_file_system.py
│   └── solidlsp
│       ├── al
│       │   └── test_al_basic.py
│       ├── bash
│       │   ├── __init__.py
│       │   └── test_bash_basic.py
│       ├── clojure
│       │   ├── __init__.py
│       │   └── test_clojure_basic.py
│       ├── csharp
│       │   └── test_csharp_basic.py
│       ├── dart
│       │   ├── __init__.py
│       │   └── test_dart_basic.py
│       ├── elixir
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_elixir_basic.py
│       │   ├── test_elixir_ignored_dirs.py
│       │   ├── test_elixir_integration.py
│       │   └── test_elixir_symbol_retrieval.py
│       ├── elm
│       │   └── test_elm_basic.py
│       ├── erlang
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_erlang_basic.py
│       │   ├── test_erlang_ignored_dirs.py
│       │   └── test_erlang_symbol_retrieval.py
│       ├── fortran
│       │   ├── __init__.py
│       │   └── test_fortran_basic.py
│       ├── fsharp
│       │   └── test_fsharp_basic.py
│       ├── go
│       │   └── test_go_basic.py
│       ├── groovy
│       │   └── test_groovy_basic.py
│       ├── haskell
│       │   ├── __init__.py
│       │   └── test_haskell_basic.py
│       ├── java
│       │   └── test_java_basic.py
│       ├── julia
│       │   └── test_julia_basic.py
│       ├── kotlin
│       │   └── test_kotlin_basic.py
│       ├── lua
│       │   └── test_lua_basic.py
│       ├── markdown
│       │   ├── __init__.py
│       │   └── test_markdown_basic.py
│       ├── matlab
│       │   ├── __init__.py
│       │   └── test_matlab_basic.py
│       ├── nix
│       │   └── test_nix_basic.py
│       ├── pascal
│       │   ├── __init__.py
│       │   └── test_pascal_basic.py
│       ├── perl
│       │   └── test_perl_basic.py
│       ├── php
│       │   └── test_php_basic.py
│       ├── powershell
│       │   ├── __init__.py
│       │   └── test_powershell_basic.py
│       ├── python
│       │   ├── test_python_basic.py
│       │   ├── test_retrieval_with_ignored_dirs.py
│       │   └── test_symbol_retrieval.py
│       ├── r
│       │   ├── __init__.py
│       │   └── test_r_basic.py
│       ├── rego
│       │   └── test_rego_basic.py
│       ├── ruby
│       │   ├── test_ruby_basic.py
│       │   └── test_ruby_symbol_retrieval.py
│       ├── rust
│       │   ├── test_rust_2024_edition.py
│       │   ├── test_rust_analyzer_detection.py
│       │   └── test_rust_basic.py
│       ├── scala
│       │   └── test_scala_language_server.py
│       ├── swift
│       │   └── test_swift_basic.py
│       ├── terraform
│       │   └── test_terraform_basic.py
│       ├── test_lsp_protocol_handler_server.py
│       ├── toml
│       │   ├── __init__.py
│       │   ├── test_toml_basic.py
│       │   ├── test_toml_edge_cases.py
│       │   ├── test_toml_ignored_dirs.py
│       │   └── test_toml_symbol_retrieval.py
│       ├── typescript
│       │   └── test_typescript_basic.py
│       ├── util
│       │   └── test_zip.py
│       ├── vue
│       │   ├── __init__.py
│       │   ├── test_vue_basic.py
│       │   ├── test_vue_error_cases.py
│       │   ├── test_vue_rename.py
│       │   └── test_vue_symbol_retrieval.py
│       ├── yaml_ls
│       │   ├── __init__.py
│       │   └── test_yaml_basic.py
│       └── zig
│           └── test_zig_basic.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/src/solidlsp/ls.py:
--------------------------------------------------------------------------------

```python
   1 | import dataclasses
   2 | import hashlib
   3 | import json
   4 | import logging
   5 | import os
   6 | import pathlib
   7 | import shutil
   8 | import subprocess
   9 | import threading
  10 | from abc import ABC, abstractmethod
  11 | from collections import defaultdict
  12 | from collections.abc import Hashable, Iterator
  13 | from contextlib import contextmanager
  14 | from copy import copy
  15 | from pathlib import Path, PurePath
  16 | from time import sleep
  17 | from typing import Self, Union, cast
  18 | 
  19 | import pathspec
  20 | from sensai.util.pickle import getstate, load_pickle
  21 | 
  22 | from serena.text_utils import MatchedConsecutiveLines
  23 | from serena.util.file_system import match_path
  24 | from solidlsp import ls_types
  25 | from solidlsp.ls_config import Language, LanguageServerConfig
  26 | from solidlsp.ls_exceptions import SolidLSPException
  27 | from solidlsp.ls_handler import SolidLanguageServerHandler
  28 | from solidlsp.ls_types import UnifiedSymbolInformation
  29 | from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils
  30 | from solidlsp.lsp_protocol_handler import lsp_types
  31 | from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes
  32 | from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants
  33 | from solidlsp.lsp_protocol_handler.lsp_types import (
  34 |     Definition,
  35 |     DefinitionParams,
  36 |     DocumentSymbol,
  37 |     LocationLink,
  38 |     RenameParams,
  39 |     SymbolInformation,
  40 | )
  41 | from solidlsp.lsp_protocol_handler.server import (
  42 |     LSPError,
  43 |     ProcessLaunchInfo,
  44 |     StringDict,
  45 | )
  46 | from solidlsp.settings import SolidLSPSettings
  47 | from solidlsp.util.cache import load_cache, save_cache
  48 | 
  49 | GenericDocumentSymbol = Union[LSPTypes.DocumentSymbol, LSPTypes.SymbolInformation, ls_types.UnifiedSymbolInformation]
  50 | log = logging.getLogger(__name__)
  51 | 
  52 | 
  53 | @dataclasses.dataclass(kw_only=True)
  54 | class ReferenceInSymbol:
  55 |     """A symbol retrieved when requesting reference to a symbol, together with the location of the reference"""
  56 | 
  57 |     symbol: ls_types.UnifiedSymbolInformation
  58 |     line: int
  59 |     character: int
  60 | 
  61 | 
  62 | @dataclasses.dataclass
  63 | class LSPFileBuffer:
  64 |     """
  65 |     This class is used to store the contents of an open LSP file in memory.
  66 |     """
  67 | 
  68 |     # uri of the file
  69 |     uri: str
  70 | 
  71 |     # The contents of the file
  72 |     contents: str
  73 | 
  74 |     # The version of the file
  75 |     version: int
  76 | 
  77 |     # The language id of the file
  78 |     language_id: str
  79 | 
  80 |     # reference count of the file
  81 |     ref_count: int
  82 | 
  83 |     content_hash: str = ""
  84 | 
  85 |     def __post_init__(self) -> None:
  86 |         self.content_hash = hashlib.md5(self.contents.encode("utf-8")).hexdigest()
  87 | 
  88 |     def split_lines(self) -> list[str]:
  89 |         """Splits the contents of the file into lines."""
  90 |         return self.contents.split("\n")
  91 | 
  92 | 
  93 | class DocumentSymbols:
  94 |     # IMPORTANT: Instances of this class are persisted in the high-level document symbol cache
  95 | 
  96 |     def __init__(self, root_symbols: list[ls_types.UnifiedSymbolInformation]):
  97 |         self.root_symbols = root_symbols
  98 |         self._all_symbols: list[ls_types.UnifiedSymbolInformation] | None = None
  99 | 
 100 |     def __getstate__(self) -> dict:
 101 |         return getstate(DocumentSymbols, self, transient_properties=["_all_symbols"])
 102 | 
 103 |     def iter_symbols(self) -> Iterator[ls_types.UnifiedSymbolInformation]:
 104 |         """
 105 |         Iterate over all symbols in the document symbol tree.
 106 |         Yields symbols in a depth-first manner.
 107 |         """
 108 |         if self._all_symbols is not None:
 109 |             yield from self._all_symbols
 110 |             return
 111 | 
 112 |         def traverse(s: ls_types.UnifiedSymbolInformation) -> Iterator[ls_types.UnifiedSymbolInformation]:
 113 |             yield s
 114 |             for child in s.get("children", []):
 115 |                 yield from traverse(child)
 116 | 
 117 |         for root_symbol in self.root_symbols:
 118 |             yield from traverse(root_symbol)
 119 | 
 120 |     def get_all_symbols_and_roots(self) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
 121 |         """
 122 |         This function returns all symbols in the document as a flat list and the root symbols.
 123 |         It exists to facilitate migration from previous versions, where this was the return interface of
 124 |         the LS method that obtained document symbols.
 125 | 
 126 |         :return: A tuple containing a list of all symbols in the document and a list of root symbols.
 127 |         """
 128 |         if self._all_symbols is None:
 129 |             self._all_symbols = list(self.iter_symbols())
 130 |         return self._all_symbols, self.root_symbols
 131 | 
 132 | 
 133 | class SolidLanguageServer(ABC):
 134 |     """
 135 |     The LanguageServer class provides a language agnostic interface to the Language Server Protocol.
 136 |     It is used to communicate with Language Servers of different programming languages.
 137 |     """
 138 | 
 139 |     CACHE_FOLDER_NAME = "cache"
 140 |     RAW_DOCUMENT_SYMBOLS_CACHE_VERSION = 1
 141 |     """
 142 |     global version identifier for raw symbol caches; an LS-specific version is defined separately and combined with this.
 143 |     This should be incremented whenever there is a change in the way raw document symbols are stored.
 144 |     If the result of a language server changes in a way that affects the raw document symbols,
 145 |     the LS-specific version should be incremented instead.
 146 |     """
 147 |     RAW_DOCUMENT_SYMBOL_CACHE_FILENAME = "raw_document_symbols.pkl"
 148 |     RAW_DOCUMENT_SYMBOL_CACHE_FILENAME_LEGACY_FALLBACK = "document_symbols_cache_v23-06-25.pkl"
 149 |     DOCUMENT_SYMBOL_CACHE_VERSION = 3
 150 |     DOCUMENT_SYMBOL_CACHE_FILENAME = "document_symbols.pkl"
 151 | 
 152 |     # To be overridden and extended by subclasses
 153 |     def is_ignored_dirname(self, dirname: str) -> bool:
 154 |         """
 155 |         A language-specific condition for directories that should always be ignored. For example, venv
 156 |         in Python and node_modules in JS/TS should be ignored always.
 157 |         """
 158 |         return dirname.startswith(".")
 159 | 
 160 |     @staticmethod
 161 |     def _determine_log_level(line: str) -> int:
 162 |         """
 163 |         Classify a stderr line from the language server to determine appropriate logging level.
 164 | 
 165 |         Language servers may emit informational messages to stderr that contain words like "error"
 166 |         but are not actual errors. Subclasses can override this method to filter out known
 167 |         false-positive patterns specific to their language server.
 168 | 
 169 |         :param line: The stderr line to classify
 170 |         :return: A logging level (logging.DEBUG, logging.INFO, logging.WARNING, or logging.ERROR)
 171 |         """
 172 |         line_lower = line.lower()
 173 | 
 174 |         # Default classification: treat lines with "error" or "exception" as ERROR level
 175 |         if "error" in line_lower or "exception" in line_lower or line.startswith("E["):
 176 |             return logging.ERROR
 177 |         else:
 178 |             return logging.INFO
 179 | 
 180 |     @classmethod
 181 |     def get_language_enum_instance(cls) -> Language:
 182 |         return Language.from_ls_class(cls)
 183 | 
 184 |     @classmethod
 185 |     def ls_resources_dir(cls, solidlsp_settings: SolidLSPSettings, mkdir: bool = True) -> str:
 186 |         """
 187 |         Returns the directory where the language server resources are downloaded.
 188 |         This is used to store language server binaries, configuration files, etc.
 189 |         """
 190 |         result = os.path.join(solidlsp_settings.ls_resources_dir, cls.__name__)
 191 | 
 192 |         # Migration of previously downloaded LS resources that were downloaded to a subdir of solidlsp instead of to the user's home
 193 |         pre_migration_ls_resources_dir = os.path.join(os.path.dirname(__file__), "language_servers", "static", cls.__name__)
 194 |         if os.path.exists(pre_migration_ls_resources_dir):
 195 |             if os.path.exists(result):
 196 |                 # if the directory already exists, we just remove the old resources
 197 |                 shutil.rmtree(result, ignore_errors=True)
 198 |             else:
 199 |                 # move old resources to the new location
 200 |                 shutil.move(pre_migration_ls_resources_dir, result)
 201 |         if mkdir:
 202 |             os.makedirs(result, exist_ok=True)
 203 |         return result
 204 | 
 205 |     @classmethod
 206 |     def create(
 207 |         cls,
 208 |         config: LanguageServerConfig,
 209 |         repository_root_path: str,
 210 |         timeout: float | None = None,
 211 |         solidlsp_settings: SolidLSPSettings | None = None,
 212 |     ) -> "SolidLanguageServer":
 213 |         """
 214 |         Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language.
 215 | 
 216 |         If language is Java, then ensure that jdk-17.0.6 or higher is installed, `java` is in PATH, and JAVA_HOME is set to the installation directory.
 217 |         If language is JS/TS, then ensure that node (v18.16.0 or higher) is installed and in PATH.
 218 | 
 219 |         :param repository_root_path: The root path of the repository.
 220 |         :param config: language server configuration.
 221 |         :param logger: The logger to use.
 222 |         :param timeout: the timeout for requests to the language server. If None, no timeout will be used.
 223 |         :param solidlsp_settings: additional settings
 224 |         :return LanguageServer: A language specific LanguageServer instance.
 225 |         """
 226 |         ls: SolidLanguageServer
 227 |         if solidlsp_settings is None:
 228 |             solidlsp_settings = SolidLSPSettings()
 229 | 
 230 |         # Ensure repository_root_path is absolute to avoid issues with file URIs
 231 |         repository_root_path = os.path.abspath(repository_root_path)
 232 | 
 233 |         ls_class = config.code_language.get_ls_class()
 234 |         # For now, we assume that all language server implementations have the same signature of the constructor
 235 |         # (which, unfortunately, differs from the signature of the base class).
 236 |         # If this assumption is ever violated, we need branching logic here.
 237 |         ls = ls_class(config, repository_root_path, solidlsp_settings)  # type: ignore
 238 |         ls.set_request_timeout(timeout)
 239 |         return ls
 240 | 
 241 |     def __init__(
 242 |         self,
 243 |         config: LanguageServerConfig,
 244 |         repository_root_path: str,
 245 |         process_launch_info: ProcessLaunchInfo,
 246 |         language_id: str,
 247 |         solidlsp_settings: SolidLSPSettings,
 248 |         cache_version_raw_document_symbols: Hashable = 1,
 249 |     ):
 250 |         """
 251 |         Initializes a LanguageServer instance.
 252 | 
 253 |         Do not instantiate this class directly. Use `LanguageServer.create` method instead.
 254 | 
 255 |         :param config: the global SolidLSP configuration.
 256 |         :param repository_root_path: the root path of the repository.
 257 |         :param process_launch_info: the command used to start the actual language server.
 258 |             The command must pass appropriate flags to the binary, so that it runs in the stdio mode,
 259 |             as opposed to HTTP, TCP modes supported by some language servers.
 260 |         :param cache_version_raw_document_symbols: the version, for caching, of the raw document symbols coming
 261 |             from this specific language server. This should be incremented by subclasses calling this constructor
 262 |             whenever the format of the raw document symbols changes (typically because the language server
 263 |             improves/fixes its output).
 264 |         """
 265 |         self._solidlsp_settings = solidlsp_settings
 266 |         lang = self.get_language_enum_instance()
 267 |         self._custom_settings = solidlsp_settings.get_ls_specific_settings(lang)
 268 |         log.debug(f"Custom config (LS-specific settings) for {lang}: {self._custom_settings}")
 269 |         self._encoding = config.encoding
 270 |         self.repository_root_path: str = repository_root_path
 271 |         log.debug(
 272 |             f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}"
 273 |         )
 274 | 
 275 |         self.language_id = language_id
 276 |         self.open_file_buffers: dict[str, LSPFileBuffer] = {}
 277 |         self.language = Language(language_id)
 278 | 
 279 |         # initialise symbol caches
 280 |         self.cache_dir = (
 281 |             Path(self.repository_root_path) / self._solidlsp_settings.project_data_relative_path / self.CACHE_FOLDER_NAME / self.language_id
 282 |         )
 283 |         self.cache_dir.mkdir(parents=True, exist_ok=True)
 284 |         # * raw document symbols cache
 285 |         self._ls_specific_raw_document_symbols_cache_version = cache_version_raw_document_symbols
 286 |         self._raw_document_symbols_cache: dict[str, tuple[str, list[DocumentSymbol] | list[SymbolInformation] | None]] = {}
 287 |         """maps relative file paths to a tuple of (file_content_hash, raw_root_symbols)"""
 288 |         self._raw_document_symbols_cache_is_modified: bool = False
 289 |         self._load_raw_document_symbols_cache()
 290 |         # * high-level document symbols cache
 291 |         self._document_symbols_cache: dict[str, tuple[str, DocumentSymbols]] = {}
 292 |         """maps relative file paths to a tuple of (file_content_hash, document_symbols)"""
 293 |         self._document_symbols_cache_is_modified: bool = False
 294 |         self._load_document_symbols_cache()
 295 | 
 296 |         self.server_started = False
 297 |         self.completions_available = threading.Event()
 298 |         if config.trace_lsp_communication:
 299 | 
 300 |             def logging_fn(source: str, target: str, msg: StringDict | str) -> None:
 301 |                 log.debug(f"LSP: {source} -> {target}: {msg!s}")
 302 | 
 303 |         else:
 304 |             logging_fn = None  # type: ignore
 305 | 
 306 |         # cmd is obtained from the child classes, which provide the language specific command to start the language server
 307 |         # LanguageServerHandler provides the functionality to start the language server and communicate with it
 308 |         log.debug(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}")
 309 |         self.server = SolidLanguageServerHandler(
 310 |             process_launch_info,
 311 |             language=self.language,
 312 |             determine_log_level=self._determine_log_level,
 313 |             logger=logging_fn,
 314 |             start_independent_lsp_process=config.start_independent_lsp_process,
 315 |         )
 316 | 
 317 |         # Set up the pathspec matcher for the ignored paths
 318 |         # for all absolute paths in ignored_paths, convert them to relative paths
 319 |         processed_patterns = []
 320 |         for pattern in set(config.ignored_paths):
 321 |             # Normalize separators (pathspec expects forward slashes)
 322 |             pattern = pattern.replace(os.path.sep, "/")
 323 |             processed_patterns.append(pattern)
 324 |         log.debug(f"Processing {len(processed_patterns)} ignored paths from the config")
 325 | 
 326 |         # Create a pathspec matcher from the processed patterns
 327 |         self._ignore_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, processed_patterns)
 328 | 
 329 |         self._request_timeout: float | None = None
 330 | 
 331 |         self._has_waited_for_cross_file_references = False
 332 | 
 333 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
 334 |         """Meant to be overridden by subclasses for LS that don't have a reliable "finished initializing" signal.
 335 | 
 336 |         LS may return incomplete results on calls to `request_references` (only references found in the same file),
 337 |         if the LS is not fully initialized yet.
 338 |         """
 339 |         return 2
 340 | 
 341 |     def set_request_timeout(self, timeout: float | None) -> None:
 342 |         """
 343 |         :param timeout: the timeout, in seconds, for requests to the language server.
 344 |         """
 345 |         self.server.set_request_timeout(timeout)
 346 | 
 347 |     def get_ignore_spec(self) -> pathspec.PathSpec:
 348 |         """
 349 |         Returns the pathspec matcher for the paths that were configured to be ignored through
 350 |         the language server configuration.
 351 | 
 352 |         This is a subset of the full language-specific ignore spec that determines
 353 |         which files are relevant for the language server.
 354 | 
 355 |         This matcher is useful for operations outside of the language server,
 356 |         such as when searching for relevant non-language files in the project.
 357 |         """
 358 |         return self._ignore_spec
 359 | 
 360 |     def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool:
 361 |         """
 362 |         Determine if a path should be ignored based on file type
 363 |         and ignore patterns.
 364 | 
 365 |         :param relative_path: Relative path to check
 366 |         :param ignore_unsupported_files: whether files that are not supported source files should be ignored
 367 | 
 368 |         :return: True if the path should be ignored, False otherwise
 369 |         """
 370 |         abs_path = os.path.join(self.repository_root_path, relative_path)
 371 |         if not os.path.exists(abs_path):
 372 |             raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed")
 373 | 
 374 |         # Check file extension if it's a file
 375 |         is_file = os.path.isfile(abs_path)
 376 |         if is_file and ignore_unsupported_files:
 377 |             fn_matcher = self.language.get_source_fn_matcher()
 378 |             if not fn_matcher.is_relevant_filename(abs_path):
 379 |                 return True
 380 | 
 381 |         # Create normalized path for consistent handling
 382 |         rel_path = Path(relative_path)
 383 | 
 384 |         # Check each part of the path against always fulfilled ignore conditions
 385 |         dir_parts = rel_path.parts
 386 |         if is_file:
 387 |             dir_parts = dir_parts[:-1]
 388 |         for part in dir_parts:
 389 |             if not part:  # Skip empty parts (e.g., from leading '/')
 390 |                 continue
 391 |             if self.is_ignored_dirname(part):
 392 |                 return True
 393 | 
 394 |         return match_path(relative_path, self.get_ignore_spec(), root_path=self.repository_root_path)
 395 | 
 396 |     def _shutdown(self, timeout: float = 5.0) -> None:
 397 |         """
 398 |         A robust shutdown process designed to terminate cleanly on all platforms, including Windows,
 399 |         by explicitly closing all I/O pipes.
 400 |         """
 401 |         if not self.server.is_running():
 402 |             log.debug("Server process not running, skipping shutdown.")
 403 |             return
 404 | 
 405 |         log.info(f"Initiating final robust shutdown with a {timeout}s timeout...")
 406 |         process = self.server.process
 407 |         if process is None:
 408 |             log.debug("Server process is None, cannot shutdown.")
 409 |             return
 410 | 
 411 |         # --- Main Shutdown Logic ---
 412 |         # Stage 1: Graceful Termination Request
 413 |         # Send LSP shutdown and close stdin to signal no more input.
 414 |         try:
 415 |             log.debug("Sending LSP shutdown request...")
 416 |             # Use a thread to timeout the LSP shutdown call since it can hang
 417 |             shutdown_thread = threading.Thread(target=self.server.shutdown)
 418 |             shutdown_thread.daemon = True
 419 |             shutdown_thread.start()
 420 |             shutdown_thread.join(timeout=2.0)  # 2 second timeout for LSP shutdown
 421 | 
 422 |             if shutdown_thread.is_alive():
 423 |                 log.debug("LSP shutdown request timed out, proceeding to terminate...")
 424 |             else:
 425 |                 log.debug("LSP shutdown request completed.")
 426 | 
 427 |             if process.stdin and not process.stdin.closed:
 428 |                 process.stdin.close()
 429 |             log.debug("Stage 1 shutdown complete.")
 430 |         except Exception as e:
 431 |             log.debug(f"Exception during graceful shutdown: {e}")
 432 |             # Ignore errors here, we are proceeding to terminate anyway.
 433 | 
 434 |         # Stage 2: Terminate and Wait for Process to Exit
 435 |         log.debug(f"Terminating process {process.pid}, current status: {process.poll()}")
 436 |         process.terminate()
 437 | 
 438 |         # Stage 3: Wait for process termination with timeout
 439 |         try:
 440 |             log.debug(f"Waiting for process {process.pid} to terminate...")
 441 |             exit_code = process.wait(timeout=timeout)
 442 |             log.info(f"Language server process terminated successfully with exit code {exit_code}.")
 443 |         except subprocess.TimeoutExpired:
 444 |             # If termination failed, forcefully kill the process
 445 |             log.warning(f"Process {process.pid} termination timed out, killing process forcefully...")
 446 |             process.kill()
 447 |             try:
 448 |                 exit_code = process.wait(timeout=2.0)
 449 |                 log.info(f"Language server process killed successfully with exit code {exit_code}.")
 450 |             except subprocess.TimeoutExpired:
 451 |                 log.error(f"Process {process.pid} could not be killed within timeout.")
 452 |         except Exception as e:
 453 |             log.error(f"Error during process shutdown: {e}")
 454 | 
 455 |     @contextmanager
 456 |     def start_server(self) -> Iterator["SolidLanguageServer"]:
 457 |         self.start()
 458 |         yield self
 459 |         self.stop()
 460 | 
 461 |     def _start_server_process(self) -> None:
 462 |         self.server_started = True
 463 |         self._start_server()
 464 | 
 465 |     @abstractmethod
 466 |     def _start_server(self) -> None:
 467 |         pass
 468 | 
 469 |     def _get_language_id_for_file(self, relative_file_path: str) -> str:
 470 |         """Return the language ID for a file.
 471 | 
 472 |         Override in subclasses to return file-specific language IDs.
 473 |         Default implementation returns self.language_id.
 474 |         """
 475 |         return self.language_id
 476 | 
 477 |     @contextmanager
 478 |     def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]:
 479 |         """
 480 |         Open a file in the Language Server. This is required before making any requests to the Language Server.
 481 | 
 482 |         :param relative_file_path: The relative path of the file to open.
 483 |         """
 484 |         if not self.server_started:
 485 |             log.error("open_file called before Language Server started")
 486 |             raise SolidLSPException("Language Server not started")
 487 | 
 488 |         absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
 489 |         uri = pathlib.Path(absolute_file_path).as_uri()
 490 | 
 491 |         if uri in self.open_file_buffers:
 492 |             assert self.open_file_buffers[uri].uri == uri
 493 |             assert self.open_file_buffers[uri].ref_count >= 1
 494 | 
 495 |             self.open_file_buffers[uri].ref_count += 1
 496 |             yield self.open_file_buffers[uri]
 497 |             self.open_file_buffers[uri].ref_count -= 1
 498 |         else:
 499 |             contents = FileUtils.read_file(absolute_file_path, self._encoding)
 500 | 
 501 |             version = 0
 502 |             language_id = self._get_language_id_for_file(relative_file_path)
 503 |             self.open_file_buffers[uri] = LSPFileBuffer(uri, contents, version, language_id, 1)
 504 | 
 505 |             self.server.notify.did_open_text_document(
 506 |                 {
 507 |                     LSPConstants.TEXT_DOCUMENT: {  # type: ignore
 508 |                         LSPConstants.URI: uri,
 509 |                         LSPConstants.LANGUAGE_ID: language_id,
 510 |                         LSPConstants.VERSION: 0,
 511 |                         LSPConstants.TEXT: contents,
 512 |                     }
 513 |                 }
 514 |             )
 515 |             yield self.open_file_buffers[uri]
 516 |             self.open_file_buffers[uri].ref_count -= 1
 517 | 
 518 |         if self.open_file_buffers[uri].ref_count == 0:
 519 |             self.server.notify.did_close_text_document(
 520 |                 {
 521 |                     LSPConstants.TEXT_DOCUMENT: {  # type: ignore
 522 |                         LSPConstants.URI: uri,
 523 |                     }
 524 |                 }
 525 |             )
 526 |             del self.open_file_buffers[uri]
 527 | 
 528 |     @contextmanager
 529 |     def _open_file_context(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> Iterator[LSPFileBuffer]:
 530 |         """
 531 |         Internal context manager to open a file, optionally reusing an existing file buffer.
 532 | 
 533 |         :param relative_file_path: the relative path of the file to open.
 534 |         :param file_buffer: an optional existing file buffer to reuse.
 535 |         """
 536 |         if file_buffer is not None:
 537 |             yield file_buffer
 538 |         else:
 539 |             with self.open_file(relative_file_path) as fb:
 540 |                 yield fb
 541 | 
 542 |     def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position:
 543 |         """
 544 |         Insert text at the given line and column in the given file and return
 545 |         the updated cursor position after inserting the text.
 546 | 
 547 |         :param relative_file_path: The relative path of the file to open.
 548 |         :param line: The line number at which text should be inserted.
 549 |         :param column: The column number at which text should be inserted.
 550 |         :param text_to_be_inserted: The text to insert.
 551 |         """
 552 |         if not self.server_started:
 553 |             log.error("insert_text_at_position called before Language Server started")
 554 |             raise SolidLSPException("Language Server not started")
 555 | 
 556 |         absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
 557 |         uri = pathlib.Path(absolute_file_path).as_uri()
 558 | 
 559 |         # Ensure the file is open
 560 |         assert uri in self.open_file_buffers
 561 | 
 562 |         file_buffer = self.open_file_buffers[uri]
 563 |         file_buffer.version += 1
 564 | 
 565 |         new_contents, new_l, new_c = TextUtils.insert_text_at_position(file_buffer.contents, line, column, text_to_be_inserted)
 566 |         file_buffer.contents = new_contents
 567 |         self.server.notify.did_change_text_document(
 568 |             {
 569 |                 LSPConstants.TEXT_DOCUMENT: {  # type: ignore
 570 |                     LSPConstants.VERSION: file_buffer.version,
 571 |                     LSPConstants.URI: file_buffer.uri,
 572 |                 },
 573 |                 LSPConstants.CONTENT_CHANGES: [
 574 |                     {
 575 |                         LSPConstants.RANGE: {
 576 |                             "start": {"line": line, "character": column},
 577 |                             "end": {"line": line, "character": column},
 578 |                         },
 579 |                         "text": text_to_be_inserted,
 580 |                     }
 581 |                 ],
 582 |             }
 583 |         )
 584 |         return ls_types.Position(line=new_l, character=new_c)
 585 | 
 586 |     def delete_text_between_positions(
 587 |         self,
 588 |         relative_file_path: str,
 589 |         start: ls_types.Position,
 590 |         end: ls_types.Position,
 591 |     ) -> str:
 592 |         """
 593 |         Delete text between the given start and end positions in the given file and return the deleted text.
 594 |         """
 595 |         if not self.server_started:
 596 |             log.error("insert_text_at_position called before Language Server started")
 597 |             raise SolidLSPException("Language Server not started")
 598 | 
 599 |         absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
 600 |         uri = pathlib.Path(absolute_file_path).as_uri()
 601 | 
 602 |         # Ensure the file is open
 603 |         assert uri in self.open_file_buffers
 604 | 
 605 |         file_buffer = self.open_file_buffers[uri]
 606 |         file_buffer.version += 1
 607 |         new_contents, deleted_text = TextUtils.delete_text_between_positions(
 608 |             file_buffer.contents, start_line=start["line"], start_col=start["character"], end_line=end["line"], end_col=end["character"]
 609 |         )
 610 |         file_buffer.contents = new_contents
 611 |         self.server.notify.did_change_text_document(
 612 |             {
 613 |                 LSPConstants.TEXT_DOCUMENT: {  # type: ignore
 614 |                     LSPConstants.VERSION: file_buffer.version,
 615 |                     LSPConstants.URI: file_buffer.uri,
 616 |                 },
 617 |                 LSPConstants.CONTENT_CHANGES: [{LSPConstants.RANGE: {"start": start, "end": end}, "text": ""}],
 618 |             }
 619 |         )
 620 |         return deleted_text
 621 | 
 622 |     def _send_definition_request(self, definition_params: DefinitionParams) -> Definition | list[LocationLink] | None:
 623 |         return self.server.send.definition(definition_params)
 624 | 
 625 |     def request_definition(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
 626 |         """
 627 |         Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server
 628 |         for the symbol at the given line and column in the given file. Wait for the response and return the result.
 629 | 
 630 |         :param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up
 631 |         :param line: The line number of the symbol
 632 |         :param column: The column number of the symbol
 633 | 
 634 |         :return: the list of locations where the symbol is defined
 635 |         """
 636 |         if not self.server_started:
 637 |             log.error("request_definition called before language server started")
 638 |             raise SolidLSPException("Language Server not started")
 639 | 
 640 |         if not self._has_waited_for_cross_file_references:
 641 |             # Some LS require waiting for a while before they can return cross-file definitions.
 642 |             # This is a workaround for such LS that don't have a reliable "finished initializing" signal.
 643 |             sleep(self._get_wait_time_for_cross_file_referencing())
 644 |             self._has_waited_for_cross_file_references = True
 645 | 
 646 |         with self.open_file(relative_file_path):
 647 |             # sending request to the language server and waiting for response
 648 |             definition_params = cast(
 649 |                 DefinitionParams,
 650 |                 {
 651 |                     LSPConstants.TEXT_DOCUMENT: {
 652 |                         LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
 653 |                     },
 654 |                     LSPConstants.POSITION: {
 655 |                         LSPConstants.LINE: line,
 656 |                         LSPConstants.CHARACTER: column,
 657 |                     },
 658 |                 },
 659 |             )
 660 |             response = self._send_definition_request(definition_params)
 661 | 
 662 |         ret: list[ls_types.Location] = []
 663 |         if isinstance(response, list):
 664 |             # response is either of type Location[] or LocationLink[]
 665 |             for item in response:
 666 |                 assert isinstance(item, dict)
 667 |                 if LSPConstants.URI in item and LSPConstants.RANGE in item:
 668 |                     new_item: dict = {}
 669 |                     new_item.update(item)
 670 |                     new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
 671 |                     new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
 672 |                     ret.append(ls_types.Location(**new_item))  # type: ignore
 673 |                 elif LSPConstants.TARGET_URI in item and LSPConstants.TARGET_RANGE in item and LSPConstants.TARGET_SELECTION_RANGE in item:
 674 |                     new_item: dict = {}  # type: ignore
 675 |                     new_item["uri"] = item[LSPConstants.TARGET_URI]  # type: ignore
 676 |                     new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
 677 |                     new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
 678 |                     new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE]  # type: ignore
 679 |                     ret.append(ls_types.Location(**new_item))  # type: ignore
 680 |                 else:
 681 |                     assert False, f"Unexpected response from Language Server: {item}"
 682 |         elif isinstance(response, dict):
 683 |             # response is of type Location
 684 |             assert LSPConstants.URI in response
 685 |             assert LSPConstants.RANGE in response
 686 | 
 687 |             new_item: dict = {}  # type: ignore
 688 |             new_item.update(response)
 689 |             new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"])
 690 |             new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path)
 691 |             ret.append(ls_types.Location(**new_item))  # type: ignore
 692 |         elif response is None:
 693 |             # Some language servers return None when they cannot find a definition
 694 |             # This is expected for certain symbol types like generics or types with incomplete information
 695 |             log.warning(f"Language server returned None for definition request at {relative_file_path}:{line}:{column}")
 696 |         else:
 697 |             assert False, f"Unexpected response from Language Server: {response}"
 698 | 
 699 |         return ret
 700 | 
 701 |     # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses
 702 |     def _send_references_request(self, relative_file_path: str, line: int, column: int) -> list[lsp_types.Location] | None:
 703 |         return self.server.send.references(
 704 |             {
 705 |                 "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))},
 706 |                 "position": {"line": line, "character": column},
 707 |                 "context": {"includeDeclaration": False},
 708 |             }
 709 |         )
 710 | 
 711 |     def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
 712 |         """
 713 |         Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server
 714 |         to find references to the symbol at the given line and column in the given file. Wait for the response and return the result.
 715 |         Filters out references located in ignored directories.
 716 | 
 717 |         :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up
 718 |         :param line: The line number of the symbol
 719 |         :param column: The column number of the symbol
 720 | 
 721 |         :return: A list of locations where the symbol is referenced (excluding ignored directories)
 722 |         """
 723 |         if not self.server_started:
 724 |             log.error("request_references called before Language Server started")
 725 |             raise SolidLSPException("Language Server not started")
 726 | 
 727 |         if not self._has_waited_for_cross_file_references:
 728 |             # Some LS require waiting for a while before they can return cross-file references.
 729 |             # This is a workaround for such LS that don't have a reliable "finished initializing" signal.
 730 |             sleep(self._get_wait_time_for_cross_file_referencing())
 731 |             self._has_waited_for_cross_file_references = True
 732 | 
 733 |         with self.open_file(relative_file_path):
 734 |             try:
 735 |                 response = self._send_references_request(relative_file_path, line=line, column=column)
 736 |             except Exception as e:
 737 |                 # Catch LSP internal error (-32603) and raise a more informative exception
 738 |                 if isinstance(e, LSPError) and getattr(e, "code", None) == -32603:
 739 |                     raise RuntimeError(
 740 |                         f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. "
 741 |                         "This often occurs when requesting references for a symbol not referenced in the expected way. "
 742 |                     ) from e
 743 |                 raise
 744 |         if response is None:
 745 |             return []
 746 | 
 747 |         ret: list[ls_types.Location] = []
 748 |         assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
 749 |         for item in response:
 750 |             assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}"
 751 |             assert LSPConstants.URI in item
 752 |             assert LSPConstants.RANGE in item
 753 | 
 754 |             abs_path = PathUtils.uri_to_path(item[LSPConstants.URI])  # type: ignore
 755 |             if not Path(abs_path).is_relative_to(self.repository_root_path):
 756 |                 log.warning(
 757 |                     "Found a reference in a path outside the repository, probably the LS is parsing things in installed packages or in the standardlib! "
 758 |                     f"Path: {abs_path}. This is a bug but we currently simply skip these references."
 759 |                 )
 760 |                 continue
 761 | 
 762 |             rel_path = Path(abs_path).relative_to(self.repository_root_path)
 763 |             if self.is_ignored_path(str(rel_path)):
 764 |                 log.debug("Ignoring reference in %s since it should be ignored", rel_path)
 765 |                 continue
 766 | 
 767 |             new_item: dict = {}
 768 |             new_item.update(item)
 769 |             new_item["absolutePath"] = str(abs_path)
 770 |             new_item["relativePath"] = str(rel_path)
 771 |             ret.append(ls_types.Location(**new_item))  # type: ignore
 772 | 
 773 |         return ret
 774 | 
 775 |     def request_text_document_diagnostics(self, relative_file_path: str) -> list[ls_types.Diagnostic]:
 776 |         """
 777 |         Raise a [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_diagnostic) request to the Language Server
 778 |         to find diagnostics for the given file. Wait for the response and return the result.
 779 | 
 780 |         :param relative_file_path: The relative path of the file to retrieve diagnostics for
 781 | 
 782 |         :return: A list of diagnostics for the file
 783 |         """
 784 |         if not self.server_started:
 785 |             log.error("request_text_document_diagnostics called before Language Server started")
 786 |             raise SolidLSPException("Language Server not started")
 787 | 
 788 |         with self.open_file(relative_file_path):
 789 |             response = self.server.send.text_document_diagnostic(
 790 |                 {
 791 |                     LSPConstants.TEXT_DOCUMENT: {  # type: ignore
 792 |                         LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri()
 793 |                     }
 794 |                 }
 795 |             )
 796 | 
 797 |         if response is None:
 798 |             return []  # type: ignore
 799 | 
 800 |         assert isinstance(response, dict), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}"
 801 |         ret: list[ls_types.Diagnostic] = []
 802 |         for item in response["items"]:  # type: ignore
 803 |             new_item: ls_types.Diagnostic = {
 804 |                 "uri": pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri(),
 805 |                 "severity": item["severity"],
 806 |                 "message": item["message"],
 807 |                 "range": item["range"],
 808 |                 "code": item["code"],  # type: ignore
 809 |             }
 810 |             ret.append(ls_types.Diagnostic(**new_item))
 811 | 
 812 |         return ret
 813 | 
 814 |     def retrieve_full_file_content(self, file_path: str) -> str:
 815 |         """
 816 |         Retrieve the full content of the given file.
 817 |         """
 818 |         if os.path.isabs(file_path):
 819 |             file_path = os.path.relpath(file_path, self.repository_root_path)
 820 |         with self.open_file(file_path) as file_data:
 821 |             return file_data.contents
 822 | 
 823 |     def retrieve_content_around_line(
 824 |         self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0
 825 |     ) -> MatchedConsecutiveLines:
 826 |         """
 827 |         Retrieve the content of the given file around the given line.
 828 | 
 829 |         :param relative_file_path: The relative path of the file to retrieve the content from
 830 |         :param line: The line number to retrieve the content around
 831 |         :param context_lines_before: The number of lines to retrieve before the given line
 832 |         :param context_lines_after: The number of lines to retrieve after the given line
 833 | 
 834 |         :return MatchedConsecutiveLines: A container with the desired lines.
 835 |         """
 836 |         with self.open_file(relative_file_path) as file_data:
 837 |             file_contents = file_data.contents
 838 |         return MatchedConsecutiveLines.from_file_contents(
 839 |             file_contents,
 840 |             line=line,
 841 |             context_lines_before=context_lines_before,
 842 |             context_lines_after=context_lines_after,
 843 |             source_file_path=relative_file_path,
 844 |         )
 845 | 
 846 |     def request_completions(
 847 |         self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False
 848 |     ) -> list[ls_types.CompletionItem]:
 849 |         """
 850 |         Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server
 851 |         to find completions at the given line and column in the given file. Wait for the response and return the result.
 852 | 
 853 |         :param relative_file_path: The relative path of the file that has the symbol for which completions should be looked up
 854 |         :param line: The line number of the symbol
 855 |         :param column: The column number of the symbol
 856 | 
 857 |         :return: A list of completions
 858 |         """
 859 |         with self.open_file(relative_file_path):
 860 |             open_file_buffer = self.open_file_buffers[pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()]
 861 |             completion_params: LSPTypes.CompletionParams = {
 862 |                 "position": {"line": line, "character": column},
 863 |                 "textDocument": {"uri": open_file_buffer.uri},
 864 |                 "context": {"triggerKind": LSPTypes.CompletionTriggerKind.Invoked},
 865 |             }
 866 |             response: list[LSPTypes.CompletionItem] | LSPTypes.CompletionList | None = None
 867 | 
 868 |             num_retries = 0
 869 |             while response is None or (response["isIncomplete"] and num_retries < 30):  # type: ignore
 870 |                 self.completions_available.wait()
 871 |                 response = self.server.send.completion(completion_params)
 872 |                 if isinstance(response, list):
 873 |                     response = {"items": response, "isIncomplete": False}
 874 |                 num_retries += 1
 875 | 
 876 |             # TODO: Understand how to appropriately handle `isIncomplete`
 877 |             if response is None or (response["isIncomplete"] and not allow_incomplete):  # type: ignore
 878 |                 return []
 879 | 
 880 |             if "items" in response:
 881 |                 response = response["items"]  # type: ignore
 882 | 
 883 |             response = cast(list[LSPTypes.CompletionItem], response)
 884 | 
 885 |             # TODO: Handle the case when the completion is a keyword
 886 |             items = [item for item in response if item["kind"] != LSPTypes.CompletionItemKind.Keyword]
 887 | 
 888 |             completions_list: list[ls_types.CompletionItem] = []
 889 | 
 890 |             for item in items:
 891 |                 assert "insertText" in item or "textEdit" in item
 892 |                 assert "kind" in item
 893 |                 completion_item = {}
 894 |                 if "detail" in item:
 895 |                     completion_item["detail"] = item["detail"]
 896 | 
 897 |                 if "label" in item:
 898 |                     completion_item["completionText"] = item["label"]
 899 |                     completion_item["kind"] = item["kind"]  # type: ignore
 900 |                 elif "insertText" in item:  # type: ignore
 901 |                     completion_item["completionText"] = item["insertText"]
 902 |                     completion_item["kind"] = item["kind"]
 903 |                 elif "textEdit" in item and "newText" in item["textEdit"]:
 904 |                     completion_item["completionText"] = item["textEdit"]["newText"]
 905 |                     completion_item["kind"] = item["kind"]
 906 |                 elif "textEdit" in item and "range" in item["textEdit"]:
 907 |                     new_dot_lineno, new_dot_colno = (
 908 |                         completion_params["position"]["line"],
 909 |                         completion_params["position"]["character"],
 910 |                     )
 911 |                     assert all(
 912 |                         (
 913 |                             item["textEdit"]["range"]["start"]["line"] == new_dot_lineno,
 914 |                             item["textEdit"]["range"]["start"]["character"] == new_dot_colno,
 915 |                             item["textEdit"]["range"]["start"]["line"] == item["textEdit"]["range"]["end"]["line"],
 916 |                             item["textEdit"]["range"]["start"]["character"] == item["textEdit"]["range"]["end"]["character"],
 917 |                         )
 918 |                     )
 919 | 
 920 |                     completion_item["completionText"] = item["textEdit"]["newText"]
 921 |                     completion_item["kind"] = item["kind"]
 922 |                 elif "textEdit" in item and "insert" in item["textEdit"]:
 923 |                     assert False
 924 |                 else:
 925 |                     assert False
 926 | 
 927 |                 completion_item = ls_types.CompletionItem(**completion_item)  # type: ignore
 928 |                 completions_list.append(completion_item)
 929 | 
 930 |             return [json.loads(json_repr) for json_repr in set(json.dumps(item, sort_keys=True) for item in completions_list)]
 931 | 
 932 |     def _request_document_symbols(
 933 |         self, relative_file_path: str, file_data: LSPFileBuffer | None
 934 |     ) -> list[SymbolInformation] | list[DocumentSymbol] | None:
 935 |         """
 936 |         Sends a [documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol)
 937 |         request to the language server to find symbols in the given file - or returns a cached result if available.
 938 | 
 939 |         :param relative_file_path: the relative path of the file that has the symbols.
 940 |         :param file_data: the file data buffer, if already opened. If None, the file will be opened in this method.
 941 |         :return: the list of root symbols in the file.
 942 |         """
 943 | 
 944 |         def get_cached_raw_document_symbols(cache_key: str, fd: LSPFileBuffer) -> list[SymbolInformation] | list[DocumentSymbol] | None:
 945 |             file_hash_and_result = self._raw_document_symbols_cache.get(cache_key)
 946 |             if file_hash_and_result is not None:
 947 |                 file_hash, result = file_hash_and_result
 948 |                 if file_hash == fd.content_hash:
 949 |                     log.debug("Returning cached raw document symbols for %s", relative_file_path)
 950 |                     return result
 951 |                 else:
 952 |                     log.debug("Document content for %s has changed (raw symbol cache is not up-to-date)", relative_file_path)
 953 |             else:
 954 |                 log.debug("No cache hit for raw document symbols symbols in %s", relative_file_path)
 955 |             return None
 956 | 
 957 |         def get_raw_document_symbols(fd: LSPFileBuffer) -> list[SymbolInformation] | list[DocumentSymbol] | None:
 958 |             # check for cached result
 959 |             cache_key = relative_file_path
 960 |             response = get_cached_raw_document_symbols(cache_key, fd)
 961 |             if response is not None:
 962 |                 return response
 963 | 
 964 |             # no cached result, query language server
 965 |             log.debug(f"Requesting document symbols for {relative_file_path} from the Language Server")
 966 |             response = self.server.send.document_symbol(
 967 |                 {"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}}
 968 |             )
 969 | 
 970 |             # update cache
 971 |             self._raw_document_symbols_cache[cache_key] = (fd.content_hash, response)
 972 |             self._raw_document_symbols_cache_is_modified = True
 973 | 
 974 |             return response
 975 | 
 976 |         if file_data is not None:
 977 |             return get_raw_document_symbols(file_data)
 978 |         else:
 979 |             with self.open_file(relative_file_path) as opened_file_data:
 980 |                 return get_raw_document_symbols(opened_file_data)
 981 | 
 982 |     def request_document_symbols(self, relative_file_path: str, file_buffer: LSPFileBuffer | None = None) -> DocumentSymbols:
 983 |         """
 984 |         Retrieves the collection of symbols in the given file
 985 | 
 986 |         :param relative_file_path: The relative path of the file that has the symbols
 987 |         :param file_buffer: an optional file buffer if the file is already opened.
 988 |         :return: the collection of symbols in the file.
 989 |             All contained symbols will have a location, children, and a parent attribute,
 990 |             where the parent attribute is None for root symbols.
 991 |             Note that this is slightly different from the call to request_full_symbol_tree,
 992 |             where the parent attribute will be the file symbol which in turn may have a package symbol as parent.
 993 |             If you need a symbol tree that contains file symbols as well, you should use `request_full_symbol_tree` instead.
 994 |         """
 995 |         with self._open_file_context(relative_file_path, file_buffer) as file_data:
 996 |             # check if the desired result is cached
 997 |             cache_key = relative_file_path
 998 |             file_hash_and_result = self._document_symbols_cache.get(cache_key)
 999 |             if file_hash_and_result is not None:
1000 |                 file_hash, document_symbols = file_hash_and_result
1001 |                 if file_hash == file_data.content_hash:
1002 |                     log.debug("Returning cached document symbols for %s", relative_file_path)
1003 |                     return document_symbols
1004 |                 else:
1005 |                     log.debug("Cached document symbol content for %s has changed", relative_file_path)
1006 |             else:
1007 |                 log.debug("No cache hit for document symbols in %s", relative_file_path)
1008 | 
1009 |             # no cached result: request the root symbols from the language server
1010 |             root_symbols = self._request_document_symbols(relative_file_path, file_data)
1011 | 
1012 |             if root_symbols is None:
1013 |                 log.warning(
1014 |                     f"Received None response from the Language Server for document symbols in {relative_file_path}. "
1015 |                     f"This means the language server can't understand this file (possibly due to syntax errors). It may also be due to a bug or misconfiguration of the LS. "
1016 |                     f"Returning empty list",
1017 |                 )
1018 |                 return DocumentSymbols([])
1019 | 
1020 |             assert isinstance(root_symbols, list), f"Unexpected response from Language Server: {root_symbols}"
1021 |             log.debug("Received %d root symbols for %s from the language server", len(root_symbols), relative_file_path)
1022 | 
1023 |             file_lines = file_data.split_lines()
1024 | 
1025 |             def convert_to_unified_symbol(original_symbol_dict: GenericDocumentSymbol) -> ls_types.UnifiedSymbolInformation:
1026 |                 """
1027 |                 Converts the given symbol dictionary to the unified representation, ensuring
1028 |                 that all required fields are present (except 'children' which is handled separately).
1029 | 
1030 |                 :param original_symbol_dict: the item to augment
1031 |                 :return: the augmented item (new object)
1032 |                 """
1033 |                 # noinspection PyInvalidCast
1034 |                 item = cast(ls_types.UnifiedSymbolInformation, dict(original_symbol_dict))
1035 |                 absolute_path = os.path.join(self.repository_root_path, relative_file_path)
1036 | 
1037 |                 # handle missing location and path entries
1038 |                 if "location" not in item:
1039 |                     uri = pathlib.Path(absolute_path).as_uri()
1040 |                     assert "range" in item
1041 |                     tree_location = ls_types.Location(
1042 |                         uri=uri,
1043 |                         range=item["range"],
1044 |                         absolutePath=absolute_path,
1045 |                         relativePath=relative_file_path,
1046 |                     )
1047 |                     item["location"] = tree_location
1048 |                 location = item["location"]
1049 |                 if "absolutePath" not in location:
1050 |                     location["absolutePath"] = absolute_path  # type: ignore
1051 |                 if "relativePath" not in location:
1052 |                     location["relativePath"] = relative_file_path  # type: ignore
1053 | 
1054 |                 if "body" not in item:
1055 |                     item["body"] = self.retrieve_symbol_body(item, file_lines=file_lines)
1056 | 
1057 |                 # handle missing selectionRange
1058 |                 if "selectionRange" not in item:
1059 |                     if "range" in item:
1060 |                         item["selectionRange"] = item["range"]
1061 |                     else:
1062 |                         item["selectionRange"] = item["location"]["range"]
1063 | 
1064 |                 return item
1065 | 
1066 |             def convert_symbols_with_common_parent(
1067 |                 symbols: list[DocumentSymbol] | list[SymbolInformation] | list[UnifiedSymbolInformation],
1068 |                 parent: ls_types.UnifiedSymbolInformation | None,
1069 |             ) -> list[ls_types.UnifiedSymbolInformation]:
1070 |                 """
1071 |                 Converts the given symbols into UnifiedSymbolInformation with proper parent-child relationships,
1072 |                 adding overload indices for symbols with the same name under the same parent.
1073 |                 """
1074 |                 total_name_counts: dict[str, int] = defaultdict(lambda: 0)
1075 |                 for symbol in symbols:
1076 |                     total_name_counts[symbol["name"]] += 1
1077 |                 name_counts: dict[str, int] = defaultdict(lambda: 0)
1078 |                 unified_symbols = []
1079 |                 for symbol in symbols:
1080 |                     usymbol = convert_to_unified_symbol(symbol)
1081 |                     if total_name_counts[usymbol["name"]] > 1:
1082 |                         usymbol["overload_idx"] = name_counts[usymbol["name"]]
1083 |                     name_counts[usymbol["name"]] += 1
1084 |                     usymbol["parent"] = parent
1085 |                     if "children" in usymbol:
1086 |                         usymbol["children"] = convert_symbols_with_common_parent(usymbol["children"], usymbol)  # type: ignore
1087 |                     else:
1088 |                         usymbol["children"] = []  # type: ignore
1089 |                     unified_symbols.append(usymbol)
1090 |                 return unified_symbols
1091 | 
1092 |             unified_root_symbols = convert_symbols_with_common_parent(root_symbols, None)
1093 |             document_symbols = DocumentSymbols(unified_root_symbols)
1094 | 
1095 |             # update cache
1096 |             log.debug("Updating cached document symbols for %s", relative_file_path)
1097 |             self._document_symbols_cache[cache_key] = (file_data.content_hash, document_symbols)
1098 |             self._document_symbols_cache_is_modified = True
1099 | 
1100 |             return document_symbols
1101 | 
1102 |     def request_full_symbol_tree(self, within_relative_path: str | None = None) -> list[ls_types.UnifiedSymbolInformation]:
1103 |         """
1104 |         Will go through all files in the project or within a relative path and build a tree of symbols.
1105 |         Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search.
1106 | 
1107 |         For each file, a symbol of kind File (2) will be created. For directories, a symbol of kind Package (4) will be created.
1108 |         All symbols will have a children attribute, thereby representing the tree structure of all symbols in the project
1109 |         that are within the repository.
1110 |         All symbols except the root packages will have a parent attribute.
1111 |         Will ignore directories starting with '.', language-specific defaults
1112 |         and user-configured directories (e.g. from .gitignore).
1113 | 
1114 |         :param within_relative_path: pass a relative path to only consider symbols within this path.
1115 |             If a file is passed, only the symbols within this file will be considered.
1116 |             If a directory is passed, all files within this directory will be considered.
1117 |         :return: A list of root symbols representing the top-level packages/modules in the project.
1118 |         """
1119 |         if within_relative_path is not None:
1120 |             within_abs_path = os.path.join(self.repository_root_path, within_relative_path)
1121 |             if not os.path.exists(within_abs_path):
1122 |                 raise FileNotFoundError(f"File or directory not found: {within_abs_path}")
1123 |             if os.path.isfile(within_abs_path):
1124 |                 if self.is_ignored_path(within_relative_path):
1125 |                     log.error("You passed a file explicitly, but it is ignored. This is probably an error. File: %s", within_relative_path)
1126 |                     return []
1127 |                 else:
1128 |                     root_nodes = self.request_document_symbols(within_relative_path).root_symbols
1129 |                     return root_nodes
1130 | 
1131 |         # Helper function to recursively process directories
1132 |         def process_directory(rel_dir_path: str) -> list[ls_types.UnifiedSymbolInformation]:
1133 |             abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path)
1134 |             abs_dir_path = os.path.realpath(abs_dir_path)
1135 | 
1136 |             if self.is_ignored_path(str(Path(abs_dir_path).relative_to(self.repository_root_path))):
1137 |                 log.debug("Skipping directory: %s (because it should be ignored)", rel_dir_path)
1138 |                 return []
1139 | 
1140 |             result = []
1141 |             try:
1142 |                 contained_dir_or_file_names = os.listdir(abs_dir_path)
1143 |             except OSError:
1144 |                 return []
1145 | 
1146 |             # Create package symbol for directory
1147 |             package_symbol = ls_types.UnifiedSymbolInformation(  # type: ignore
1148 |                 name=os.path.basename(abs_dir_path),
1149 |                 kind=ls_types.SymbolKind.Package,
1150 |                 location=ls_types.Location(
1151 |                     uri=str(pathlib.Path(abs_dir_path).as_uri()),
1152 |                     range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}},
1153 |                     absolutePath=str(abs_dir_path),
1154 |                     relativePath=str(Path(abs_dir_path).resolve().relative_to(self.repository_root_path)),
1155 |                 ),
1156 |                 children=[],
1157 |             )
1158 |             result.append(package_symbol)
1159 | 
1160 |             for contained_dir_or_file_name in contained_dir_or_file_names:
1161 |                 contained_dir_or_file_abs_path = os.path.join(abs_dir_path, contained_dir_or_file_name)
1162 | 
1163 |                 # obtain relative path
1164 |                 try:
1165 |                     contained_dir_or_file_rel_path = str(
1166 |                         Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)
1167 |                     )
1168 |                 except ValueError as e:
1169 |                     # Typically happens when the path is not under the repository root (e.g., symlink pointing outside)
1170 |                     log.warning(
1171 |                         "Skipping path %s; likely outside of the repository root %s [cause: %s]",
1172 |                         contained_dir_or_file_abs_path,
1173 |                         self.repository_root_path,
1174 |                         e,
1175 |                     )
1176 |                     continue
1177 | 
1178 |                 if self.is_ignored_path(contained_dir_or_file_rel_path):
1179 |                     log.debug("Skipping item: %s (because it should be ignored)", contained_dir_or_file_rel_path)
1180 |                     continue
1181 | 
1182 |                 if os.path.isdir(contained_dir_or_file_abs_path):
1183 |                     child_symbols = process_directory(contained_dir_or_file_rel_path)
1184 |                     package_symbol["children"].extend(child_symbols)
1185 |                     for child in child_symbols:
1186 |                         child["parent"] = package_symbol
1187 | 
1188 |                 elif os.path.isfile(contained_dir_or_file_abs_path):
1189 |                     with self._open_file_context(contained_dir_or_file_rel_path) as file_data:
1190 |                         document_symbols = self.request_document_symbols(contained_dir_or_file_rel_path, file_data)
1191 |                         file_root_nodes = document_symbols.root_symbols
1192 | 
1193 |                         # Create file symbol, link with children
1194 |                         file_range = self._get_range_from_file_content(file_data.contents)
1195 |                         file_symbol = ls_types.UnifiedSymbolInformation(  # type: ignore
1196 |                             name=os.path.splitext(contained_dir_or_file_name)[0],
1197 |                             kind=ls_types.SymbolKind.File,
1198 |                             range=file_range,
1199 |                             selectionRange=file_range,
1200 |                             location=ls_types.Location(
1201 |                                 uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()),
1202 |                                 range=file_range,
1203 |                                 absolutePath=str(contained_dir_or_file_abs_path),
1204 |                                 relativePath=str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)),
1205 |                             ),
1206 |                             children=file_root_nodes,
1207 |                             parent=package_symbol,
1208 |                         )
1209 |                         for child in file_root_nodes:
1210 |                             child["parent"] = file_symbol
1211 | 
1212 |                     # Link file symbol with package
1213 |                     package_symbol["children"].append(file_symbol)
1214 | 
1215 |                     # TODO: Not sure if this is actually still needed given recent changes to relative path handling
1216 |                     def fix_relative_path(nodes: list[ls_types.UnifiedSymbolInformation]) -> None:
1217 |                         for node in nodes:
1218 |                             if "location" in node and "relativePath" in node["location"]:
1219 |                                 path = Path(node["location"]["relativePath"])  # type: ignore
1220 |                                 if path.is_absolute():
1221 |                                     try:
1222 |                                         path = path.relative_to(self.repository_root_path)
1223 |                                         node["location"]["relativePath"] = str(path)
1224 |                                     except Exception:
1225 |                                         pass
1226 |                             if "children" in node:
1227 |                                 fix_relative_path(node["children"])
1228 | 
1229 |                     fix_relative_path(file_root_nodes)
1230 | 
1231 |             return result
1232 | 
1233 |         # Start from the root or the specified directory
1234 |         start_rel_path = within_relative_path or "."
1235 |         return process_directory(start_rel_path)
1236 | 
1237 |     @staticmethod
1238 |     def _get_range_from_file_content(file_content: str) -> ls_types.Range:
1239 |         """
1240 |         Get the range for the given file.
1241 |         """
1242 |         lines = file_content.split("\n")
1243 |         end_line = len(lines)
1244 |         end_column = len(lines[-1])
1245 |         return ls_types.Range(start=ls_types.Position(line=0, character=0), end=ls_types.Position(line=end_line, character=end_column))
1246 | 
1247 |     def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[UnifiedSymbolInformation]]:
1248 |         """
1249 |         :return: A mapping of all relative paths analyzed to lists of top-level symbols in the corresponding file.
1250 |         """
1251 |         symbol_tree = self.request_full_symbol_tree(relative_dir_path)
1252 |         # Initialize result dictionary
1253 |         result: dict[str, list[UnifiedSymbolInformation]] = defaultdict(list)
1254 | 
1255 |         # Helper function to process a symbol and its children
1256 |         def process_symbol(symbol: ls_types.UnifiedSymbolInformation) -> None:
1257 |             if symbol["kind"] == ls_types.SymbolKind.File:
1258 |                 # For file symbols, process their children (top-level symbols)
1259 |                 for child in symbol["children"]:
1260 |                     # Handle cross-platform path resolution (fixes Docker/macOS path issues)
1261 |                     absolute_path = Path(child["location"]["absolutePath"]).resolve()
1262 |                     repository_root = Path(self.repository_root_path).resolve()
1263 | 
1264 |                     # Try pathlib first, fallback to alternative approach if paths are incompatible
1265 |                     try:
1266 |                         path = absolute_path.relative_to(repository_root)
1267 |                     except ValueError:
1268 |                         # If paths are from different roots (e.g., /workspaces vs /Users),
1269 |                         # use the relativePath from location if available, or extract from absolutePath
1270 |                         if "relativePath" in child["location"] and child["location"]["relativePath"]:
1271 |                             path = Path(child["location"]["relativePath"])
1272 |                         else:
1273 |                             # Extract relative path by finding common structure
1274 |                             # Example: /workspaces/.../test_repo/file.py -> test_repo/file.py
1275 |                             path_parts = absolute_path.parts
1276 | 
1277 |                             # Find the last common part or use a fallback
1278 |                             if "test_repo" in path_parts:
1279 |                                 test_repo_idx = path_parts.index("test_repo")
1280 |                                 path = Path(*path_parts[test_repo_idx:])
1281 |                             else:
1282 |                                 # Last resort: use filename only
1283 |                                 path = Path(absolute_path.name)
1284 |                     result[str(path)].append(child)
1285 |             # For package/directory symbols, process their children
1286 |             for child in symbol["children"]:
1287 |                 process_symbol(child)
1288 | 
1289 |         # Process each root symbol
1290 |         for root in symbol_tree:
1291 |             process_symbol(root)
1292 |         return result
1293 | 
1294 |     def request_document_overview(self, relative_file_path: str) -> list[UnifiedSymbolInformation]:
1295 |         """
1296 |         :return: the top-level symbols in the given file.
1297 |         """
1298 |         return self.request_document_symbols(relative_file_path).root_symbols
1299 | 
1300 |     def request_overview(self, within_relative_path: str) -> dict[str, list[UnifiedSymbolInformation]]:
1301 |         """
1302 |         An overview of all symbols in the given file or directory.
1303 | 
1304 |         :param within_relative_path: the relative path to the file or directory to get the overview of.
1305 |         :return: A mapping of all relative paths analyzed to lists of top-level symbols in the corresponding file.
1306 |         """
1307 |         abs_path = (Path(self.repository_root_path) / within_relative_path).resolve()
1308 |         if not abs_path.exists():
1309 |             raise FileNotFoundError(f"File or directory not found: {abs_path}")
1310 | 
1311 |         if abs_path.is_file():
1312 |             symbols_overview = self.request_document_overview(within_relative_path)
1313 |             return {within_relative_path: symbols_overview}
1314 |         else:
1315 |             return self.request_dir_overview(within_relative_path)
1316 | 
1317 |     def request_hover(self, relative_file_path: str, line: int, column: int) -> ls_types.Hover | None:
1318 |         """
1319 |         Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server
1320 |         to find the hover information at the given line and column in the given file. Wait for the response and return the result.
1321 | 
1322 |         :param relative_file_path: The relative path of the file that has the hover information
1323 |         :param line: The line number of the symbol
1324 |         :param column: The column number of the symbol
1325 | 
1326 |         :return None
1327 |         """
1328 |         with self.open_file(relative_file_path):
1329 |             response = self.server.send.hover(
1330 |                 {
1331 |                     "textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()},
1332 |                     "position": {
1333 |                         "line": line,
1334 |                         "character": column,
1335 |                     },
1336 |                 }
1337 |             )
1338 | 
1339 |         if response is None:
1340 |             return None
1341 | 
1342 |         assert isinstance(response, dict)
1343 | 
1344 |         return ls_types.Hover(**response)  # type: ignore
1345 | 
1346 |     def retrieve_symbol_body(
1347 |         self,
1348 |         symbol: ls_types.UnifiedSymbolInformation | LSPTypes.SymbolInformation,
1349 |         file_lines: list[str] | None = None,
1350 |         file_buffer: LSPFileBuffer | None = None,
1351 |     ) -> str:
1352 |         """
1353 |         Load the body of the given symbol. If the body is already contained in the symbol, just return it.
1354 |         """
1355 |         existing_body = symbol.get("body", None)
1356 |         if existing_body:
1357 |             return str(existing_body)
1358 | 
1359 |         assert "location" in symbol
1360 |         symbol_start_line = symbol["location"]["range"]["start"]["line"]
1361 |         symbol_end_line = symbol["location"]["range"]["end"]["line"]
1362 |         assert "relativePath" in symbol["location"]
1363 |         if file_lines is None:
1364 |             with self._open_file_context(symbol["location"]["relativePath"], file_buffer) as f:  # type: ignore
1365 |                 file_lines = f.split_lines()
1366 |         symbol_body = "\n".join(file_lines[symbol_start_line : symbol_end_line + 1])
1367 | 
1368 |         # remove leading indentation
1369 |         symbol_start_column = symbol["location"]["range"]["start"]["character"]  # type: ignore
1370 |         symbol_body = symbol_body[symbol_start_column:]
1371 |         return symbol_body
1372 | 
1373 |     def request_referencing_symbols(
1374 |         self,
1375 |         relative_file_path: str,
1376 |         line: int,
1377 |         column: int,
1378 |         include_imports: bool = True,
1379 |         include_self: bool = False,
1380 |         include_body: bool = False,
1381 |         include_file_symbols: bool = False,
1382 |     ) -> list[ReferenceInSymbol]:
1383 |         """
1384 |         Finds all symbols that reference the symbol at the given location.
1385 |         This is similar to request_references but filters to only include symbols
1386 |         (functions, methods, classes, etc.) that reference the target symbol.
1387 | 
1388 |         :param relative_file_path: The relative path to the file.
1389 |         :param line: The 0-indexed line number.
1390 |         :param column: The 0-indexed column number.
1391 |         :param include_imports: whether to also include imports as references.
1392 |             Unfortunately, the LSP does not have an import type, so the references corresponding to imports
1393 |             will not be easily distinguishable from definitions.
1394 |         :param include_self: whether to include the references that is the "input symbol" itself.
1395 |             Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition.
1396 |         :param include_body: whether to include the body of the symbols in the result.
1397 |         :param include_file_symbols: whether to include references that are file symbols. This
1398 |             is often a fallback mechanism for when the reference cannot be resolved to a symbol.
1399 |         :return: List of objects containing the symbol and the location of the reference.
1400 |         """
1401 |         if not self.server_started:
1402 |             log.error("request_referencing_symbols called before Language Server started")
1403 |             raise SolidLSPException("Language Server not started")
1404 | 
1405 |         # First, get all references to the symbol
1406 |         references = self.request_references(relative_file_path, line, column)
1407 |         if not references:
1408 |             return []
1409 | 
1410 |         # For each reference, find the containing symbol
1411 |         result = []
1412 |         incoming_symbol = None
1413 |         for ref in references:
1414 |             ref_path = ref["relativePath"]
1415 |             assert ref_path is not None
1416 |             ref_line = ref["range"]["start"]["line"]
1417 |             ref_col = ref["range"]["start"]["character"]
1418 | 
1419 |             with self.open_file(ref_path) as file_data:
1420 |                 # Get the containing symbol for this reference
1421 |                 containing_symbol = self.request_containing_symbol(ref_path, ref_line, ref_col, include_body=include_body)
1422 |                 if containing_symbol is None:
1423 |                     # TODO: HORRIBLE HACK! I don't know how to do it better for now...
1424 |                     # THIS IS BOUND TO BREAK IN MANY CASES! IT IS ALSO SPECIFIC TO PYTHON!
1425 |                     # Background:
1426 |                     # When a variable is used to change something, like
1427 |                     #
1428 |                     # instance = MyClass()
1429 |                     # instance.status = "new status"
1430 |                     #
1431 |                     # we can't find the containing symbol for the reference to `status`
1432 |                     # since there is no container on the line of the reference
1433 |                     # The hack is to try to find a variable symbol in the containing module
1434 |                     # by using the text of the reference to find the variable name (In a very heuristic way)
1435 |                     # and then look for a symbol with that name and kind Variable
1436 |                     ref_text = file_data.contents.split("\n")[ref_line]
1437 |                     if "." in ref_text:
1438 |                         containing_symbol_name = ref_text.split(".")[0]
1439 |                         document_symbols = self.request_document_symbols(ref_path)
1440 |                         for symbol in document_symbols.iter_symbols():
1441 |                             if symbol["name"] == containing_symbol_name and symbol["kind"] == ls_types.SymbolKind.Variable:
1442 |                                 containing_symbol = copy(symbol)
1443 |                                 containing_symbol["location"] = ref
1444 |                                 containing_symbol["range"] = ref["range"]
1445 |                                 break
1446 | 
1447 |                 # We failed retrieving the symbol, falling back to creating a file symbol
1448 |                 if containing_symbol is None and include_file_symbols:
1449 |                     log.warning(f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}. Returning file symbol instead")
1450 |                     fileRange = self._get_range_from_file_content(file_data.contents)
1451 |                     location = ls_types.Location(
1452 |                         uri=str(pathlib.Path(os.path.join(self.repository_root_path, ref_path)).as_uri()),
1453 |                         range=fileRange,
1454 |                         absolutePath=str(os.path.join(self.repository_root_path, ref_path)),
1455 |                         relativePath=ref_path,
1456 |                     )
1457 |                     name = os.path.splitext(os.path.basename(ref_path))[0]
1458 | 
1459 |                     if include_body:
1460 |                         body = self.retrieve_full_file_content(ref_path)
1461 |                     else:
1462 |                         body = ""
1463 | 
1464 |                     containing_symbol = ls_types.UnifiedSymbolInformation(
1465 |                         kind=ls_types.SymbolKind.File,
1466 |                         range=fileRange,
1467 |                         selectionRange=fileRange,
1468 |                         location=location,
1469 |                         name=name,
1470 |                         children=[],
1471 |                         body=body,
1472 |                     )
1473 |                 if containing_symbol is None or (not include_file_symbols and containing_symbol["kind"] == ls_types.SymbolKind.File):
1474 |                     continue
1475 | 
1476 |                 assert "location" in containing_symbol
1477 |                 assert "selectionRange" in containing_symbol
1478 | 
1479 |                 # Checking for self-reference
1480 |                 if (
1481 |                     containing_symbol["location"]["relativePath"] == relative_file_path
1482 |                     and containing_symbol["selectionRange"]["start"]["line"] == ref_line
1483 |                     and containing_symbol["selectionRange"]["start"]["character"] == ref_col
1484 |                 ):
1485 |                     incoming_symbol = containing_symbol
1486 |                     if include_self:
1487 |                         result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col))
1488 |                         continue
1489 |                     log.debug(f"Found self-reference for {incoming_symbol['name']}, skipping it since {include_self=}")
1490 |                     continue
1491 | 
1492 |                 # checking whether reference is an import
1493 |                 # This is neither really safe nor elegant, but if we don't do it,
1494 |                 # there is no way to distinguish between definitions and imports as import is not a symbol-type
1495 |                 # and we get the type referenced symbol resulting from imports...
1496 |                 if (
1497 |                     not include_imports
1498 |                     and incoming_symbol is not None
1499 |                     and containing_symbol["name"] == incoming_symbol["name"]
1500 |                     and containing_symbol["kind"] == incoming_symbol["kind"]
1501 |                 ):
1502 |                     log.debug(
1503 |                         f"Found import of referenced symbol {incoming_symbol['name']}"
1504 |                         f"in {containing_symbol['location']['relativePath']}, skipping"
1505 |                     )
1506 |                     continue
1507 | 
1508 |                 result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col))
1509 | 
1510 |         return result
1511 | 
1512 |     def request_containing_symbol(
1513 |         self,
1514 |         relative_file_path: str,
1515 |         line: int,
1516 |         column: int | None = None,
1517 |         strict: bool = False,
1518 |         include_body: bool = False,
1519 |     ) -> ls_types.UnifiedSymbolInformation | None:
1520 |         """
1521 |         Finds the first symbol containing the position for the given file.
1522 |         For Python, container symbols are considered to be those with kinds corresponding to
1523 |         functions, methods, or classes (typically: Function (12), Method (6), Class (5)).
1524 | 
1525 |         The method operates as follows:
1526 |           - Request the document symbols for the file.
1527 |           - Filter symbols to those that start at or before the given line.
1528 |           - From these, first look for symbols whose range contains the (line, column).
1529 |           - If one or more symbols contain the position, return the one with the greatest starting position
1530 |             (i.e. the innermost container).
1531 |           - If none (strictly) contain the position, return the symbol with the greatest starting position
1532 |             among those above the given line.
1533 |           - If no container candidates are found, return None.
1534 | 
1535 |         :param relative_file_path: The relative path to the Python file.
1536 |         :param line: The 0-indexed line number.
1537 |         :param column: The 0-indexed column (also called character). If not passed, the lookup will be based
1538 |             only on the line.
1539 |         :param strict: If True, the position must be strictly within the range of the symbol.
1540 |             Setting to True is useful for example for finding the parent of a symbol, as with strict=False,
1541 |             and the line pointing to a symbol itself, the containing symbol will be the symbol itself
1542 |             (and not the parent).
1543 |         :param include_body: Whether to include the body of the symbol in the result.
1544 |         :return: The container symbol (if found) or None.
1545 |         """
1546 |         # checking if the line is empty, unfortunately ugly and duplicating code, but I don't want to refactor
1547 |         with self.open_file(relative_file_path):
1548 |             absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
1549 |             content = FileUtils.read_file(absolute_file_path, self._encoding)
1550 |             if content.split("\n")[line].strip() == "":
1551 |                 log.error(f"Passing empty lines to request_container_symbol is currently not supported, {relative_file_path=}, {line=}")
1552 |                 return None
1553 | 
1554 |         document_symbols = self.request_document_symbols(relative_file_path)
1555 | 
1556 |         # make jedi and pyright api compatible
1557 |         # the former has no location, the later has no range
1558 |         # we will just always add location of the desired format to all symbols
1559 |         for symbol in document_symbols.iter_symbols():
1560 |             if "location" not in symbol:
1561 |                 range = symbol["range"]
1562 |                 location = ls_types.Location(
1563 |                     uri=f"file:/{absolute_file_path}",
1564 |                     range=range,
1565 |                     absolutePath=absolute_file_path,
1566 |                     relativePath=relative_file_path,
1567 |                 )
1568 |                 symbol["location"] = location
1569 |             else:
1570 |                 location = symbol["location"]
1571 |                 assert "range" in location
1572 |                 location["absolutePath"] = absolute_file_path
1573 |                 location["relativePath"] = relative_file_path
1574 |                 location["uri"] = Path(absolute_file_path).as_uri()
1575 | 
1576 |         # Allowed container kinds, currently only for Python
1577 |         container_symbol_kinds = {ls_types.SymbolKind.Method, ls_types.SymbolKind.Function, ls_types.SymbolKind.Class}
1578 | 
1579 |         def is_position_in_range(line: int, range_d: ls_types.Range) -> bool:
1580 |             start = range_d["start"]
1581 |             end = range_d["end"]
1582 | 
1583 |             column_condition = True
1584 |             if strict:
1585 |                 line_condition = end["line"] >= line > start["line"]
1586 |                 if column is not None and line == start["line"]:
1587 |                     column_condition = column > start["character"]
1588 |             else:
1589 |                 line_condition = end["line"] >= line >= start["line"]
1590 |                 if column is not None and line == start["line"]:
1591 |                     column_condition = column >= start["character"]
1592 |             return line_condition and column_condition
1593 | 
1594 |         # Only consider containers that are not one-liners (otherwise we may get imports)
1595 |         candidate_containers = [
1596 |             s
1597 |             for s in document_symbols.iter_symbols()
1598 |             if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"]
1599 |         ]
1600 |         var_containers = [s for s in document_symbols.iter_symbols() if s["kind"] == ls_types.SymbolKind.Variable]
1601 |         candidate_containers.extend(var_containers)
1602 | 
1603 |         if not candidate_containers:
1604 |             return None
1605 | 
1606 |         # From the candidates, find those whose range contains the given position.
1607 |         containing_symbols = []
1608 |         for symbol in candidate_containers:
1609 |             s_range = symbol["location"]["range"]
1610 |             if not is_position_in_range(line, s_range):
1611 |                 continue
1612 |             containing_symbols.append(symbol)
1613 | 
1614 |         if containing_symbols:
1615 |             # Return the one with the greatest starting position (i.e. the innermost container).
1616 |             containing_symbol = max(containing_symbols, key=lambda s: s["location"]["range"]["start"]["line"])
1617 |             if include_body:
1618 |                 containing_symbol["body"] = self.retrieve_symbol_body(containing_symbol)
1619 |             return containing_symbol
1620 |         else:
1621 |             return None
1622 | 
1623 |     def request_container_of_symbol(
1624 |         self, symbol: ls_types.UnifiedSymbolInformation, include_body: bool = False
1625 |     ) -> ls_types.UnifiedSymbolInformation | None:
1626 |         """
1627 |         Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned
1628 |         without further searching.
1629 | 
1630 |         :param symbol: The symbol to find the container of.
1631 |         :param include_body: whether to include the body of the symbol in the result.
1632 |         :return: The container of the given symbol or None if no container is found.
1633 |         """
1634 |         if "parent" in symbol:
1635 |             return symbol["parent"]
1636 |         assert "location" in symbol, f"Symbol {symbol} has no location and no parent attribute"
1637 |         return self.request_containing_symbol(
1638 |             symbol["location"]["relativePath"],  # type: ignore
1639 |             symbol["location"]["range"]["start"]["line"],
1640 |             symbol["location"]["range"]["start"]["character"],
1641 |             strict=True,
1642 |             include_body=include_body,
1643 |         )
1644 | 
1645 |     def _get_preferred_definition(self, definitions: list[ls_types.Location]) -> ls_types.Location:
1646 |         """
1647 |         Select the preferred definition from a list of definitions.
1648 | 
1649 |         When multiple definitions are returned (e.g., both source and type definitions),
1650 |         this method determines which one to use. The base implementation simply returns
1651 |         the first definition.
1652 | 
1653 |         Subclasses can override this method to implement language-specific preferences.
1654 |         For example, TypeScript/Vue servers may prefer source files over .d.ts type
1655 |         definition files.
1656 | 
1657 |         :param definitions: A non-empty list of definition locations.
1658 |         :return: The preferred definition location.
1659 |         """
1660 |         return definitions[0]
1661 | 
1662 |     def request_defining_symbol(
1663 |         self,
1664 |         relative_file_path: str,
1665 |         line: int,
1666 |         column: int,
1667 |         include_body: bool = False,
1668 |     ) -> ls_types.UnifiedSymbolInformation | None:
1669 |         """
1670 |         Finds the symbol that defines the symbol at the given location.
1671 | 
1672 |         This method first finds the definition of the symbol at the given position,
1673 |         then retrieves the full symbol information for that definition.
1674 | 
1675 |         :param relative_file_path: The relative path to the file.
1676 |         :param line: The 0-indexed line number.
1677 |         :param column: The 0-indexed column number.
1678 |         :param include_body: whether to include the body of the symbol in the result.
1679 |         :return: The symbol information for the definition, or None if not found.
1680 |         """
1681 |         if not self.server_started:
1682 |             log.error("request_defining_symbol called before language server started")
1683 |             raise SolidLSPException("Language Server not started")
1684 | 
1685 |         # Get the definition location(s)
1686 |         definitions = self.request_definition(relative_file_path, line, column)
1687 |         if not definitions:
1688 |             return None
1689 | 
1690 |         # Select the preferred definition (subclasses can override _get_preferred_definition)
1691 |         definition = self._get_preferred_definition(definitions)
1692 |         def_path = definition["relativePath"]
1693 |         assert def_path is not None
1694 |         def_line = definition["range"]["start"]["line"]
1695 |         def_col = definition["range"]["start"]["character"]
1696 | 
1697 |         # Find the symbol at or containing this location
1698 |         defining_symbol = self.request_containing_symbol(def_path, def_line, def_col, strict=False, include_body=include_body)
1699 | 
1700 |         return defining_symbol
1701 | 
1702 |     def _save_raw_document_symbols_cache(self) -> None:
1703 |         cache_file = self.cache_dir / self.RAW_DOCUMENT_SYMBOL_CACHE_FILENAME
1704 | 
1705 |         if not self._raw_document_symbols_cache_is_modified:
1706 |             log.debug("No changes to raw document symbols cache, skipping save")
1707 |             return
1708 | 
1709 |         log.info("Saving updated raw document symbols cache to %s", cache_file)
1710 |         try:
1711 |             save_cache(str(cache_file), self._raw_document_symbols_cache_version(), self._raw_document_symbols_cache)
1712 |             self._raw_document_symbols_cache_is_modified = False
1713 |         except Exception as e:
1714 |             log.error(
1715 |                 "Failed to save raw document symbols cache to %s: %s. Note: this may have resulted in a corrupted cache file.",
1716 |                 cache_file,
1717 |                 e,
1718 |             )
1719 | 
1720 |     def _raw_document_symbols_cache_version(self) -> tuple[int, Hashable]:
1721 |         return (self.RAW_DOCUMENT_SYMBOLS_CACHE_VERSION, self._ls_specific_raw_document_symbols_cache_version)
1722 | 
1723 |     def _load_raw_document_symbols_cache(self) -> None:
1724 |         cache_file = self.cache_dir / self.RAW_DOCUMENT_SYMBOL_CACHE_FILENAME
1725 | 
1726 |         if not cache_file.exists():
1727 |             # check for legacy cache to load to migrate
1728 |             legacy_cache_file = self.cache_dir / self.RAW_DOCUMENT_SYMBOL_CACHE_FILENAME_LEGACY_FALLBACK
1729 |             if legacy_cache_file.exists():
1730 |                 try:
1731 |                     legacy_cache: dict[
1732 |                         str, tuple[str, tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]]
1733 |                     ] = load_pickle(legacy_cache_file)
1734 |                     log.info("Migrating legacy document symbols cache with %d entries", len(legacy_cache))
1735 |                     num_symbols_migrated = 0
1736 |                     migrated_cache = {}
1737 |                     for cache_key, (file_hash, (all_symbols, root_symbols)) in legacy_cache.items():
1738 |                         if cache_key.endswith("-True"):  # include_body=True
1739 |                             new_cache_key = cache_key[:-5]
1740 |                             migrated_cache[new_cache_key] = (file_hash, root_symbols)
1741 |                             num_symbols_migrated += len(all_symbols)
1742 |                     log.info("Migrated %d document symbols from legacy cache", num_symbols_migrated)
1743 |                     self._raw_document_symbols_cache = migrated_cache  # type: ignore
1744 |                     self._raw_document_symbols_cache_is_modified = True
1745 |                     self._save_raw_document_symbols_cache()
1746 |                     legacy_cache_file.unlink()
1747 |                     return
1748 |                 except Exception as e:
1749 |                     log.error("Error during cache migration: %s", e)
1750 |                     return
1751 | 
1752 |         # load existing cache (if any)
1753 |         if cache_file.exists():
1754 |             log.info("Loading document symbols cache from %s", cache_file)
1755 |             try:
1756 |                 saved_cache = load_cache(str(cache_file), self._raw_document_symbols_cache_version())
1757 |                 if saved_cache is not None:
1758 |                     self._raw_document_symbols_cache = saved_cache
1759 |                     log.info(f"Loaded {len(self._raw_document_symbols_cache)} entries from raw document symbols cache.")
1760 |             except Exception as e:
1761 |                 # cache can become corrupt, so just skip loading it
1762 |                 log.warning(
1763 |                     "Failed to load raw document symbols cache from %s (%s); Ignoring cache.",
1764 |                     cache_file,
1765 |                     e,
1766 |                 )
1767 | 
1768 |     def _save_document_symbols_cache(self) -> None:
1769 |         cache_file = self.cache_dir / self.DOCUMENT_SYMBOL_CACHE_FILENAME
1770 | 
1771 |         if not self._document_symbols_cache_is_modified:
1772 |             log.debug("No changes to document symbols cache, skipping save")
1773 |             return
1774 | 
1775 |         log.info("Saving updated document symbols cache to %s", cache_file)
1776 |         try:
1777 |             save_cache(str(cache_file), self.DOCUMENT_SYMBOL_CACHE_VERSION, self._document_symbols_cache)
1778 |             self._document_symbols_cache_is_modified = False
1779 |         except Exception as e:
1780 |             log.error(
1781 |                 "Failed to save document symbols cache to %s: %s. Note: this may have resulted in a corrupted cache file.",
1782 |                 cache_file,
1783 |                 e,
1784 |             )
1785 | 
1786 |     def _load_document_symbols_cache(self) -> None:
1787 |         cache_file = self.cache_dir / self.DOCUMENT_SYMBOL_CACHE_FILENAME
1788 |         if cache_file.exists():
1789 |             log.info("Loading document symbols cache from %s", cache_file)
1790 |             try:
1791 |                 saved_cache = load_cache(str(cache_file), self.DOCUMENT_SYMBOL_CACHE_VERSION)
1792 |                 if saved_cache is not None:
1793 |                     self._document_symbols_cache = saved_cache
1794 |                     log.info(f"Loaded {len(self._document_symbols_cache)} entries from document symbols cache.")
1795 |             except Exception as e:
1796 |                 # cache can become corrupt, so just skip loading it
1797 |                 log.warning(
1798 |                     "Failed to load document symbols cache from %s (%s); Ignoring cache.",
1799 |                     cache_file,
1800 |                     e,
1801 |                 )
1802 | 
1803 |     def save_cache(self) -> None:
1804 |         self._save_raw_document_symbols_cache()
1805 |         self._save_document_symbols_cache()
1806 | 
1807 |     def request_workspace_symbol(self, query: str) -> list[ls_types.UnifiedSymbolInformation] | None:
1808 |         """
1809 |         Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server
1810 |         to find symbols across the whole workspace. Wait for the response and return the result.
1811 | 
1812 |         :param query: The query string to filter symbols by
1813 | 
1814 |         :return: A list of matching symbols
1815 |         """
1816 |         response = self.server.send.workspace_symbol({"query": query})
1817 |         if response is None:
1818 |             return None
1819 | 
1820 |         assert isinstance(response, list)
1821 | 
1822 |         ret: list[ls_types.UnifiedSymbolInformation] = []
1823 |         for item in response:
1824 |             assert isinstance(item, dict)
1825 | 
1826 |             assert LSPConstants.NAME in item
1827 |             assert LSPConstants.KIND in item
1828 |             assert LSPConstants.LOCATION in item
1829 | 
1830 |             ret.append(ls_types.UnifiedSymbolInformation(**item))  # type: ignore
1831 | 
1832 |         return ret
1833 | 
1834 |     def request_rename_symbol_edit(
1835 |         self,
1836 |         relative_file_path: str,
1837 |         line: int,
1838 |         column: int,
1839 |         new_name: str,
1840 |     ) -> ls_types.WorkspaceEdit | None:
1841 |         """
1842 |         Retrieve a WorkspaceEdit for renaming the symbol at the given location to the new name.
1843 |         Does not apply the edit, just retrieves it. In order to actually rename the symbol, call apply_workspace_edit.
1844 | 
1845 |         :param relative_file_path: The relative path to the file containing the symbol
1846 |         :param line: The 0-indexed line number of the symbol
1847 |         :param column: The 0-indexed column number of the symbol
1848 |         :param new_name: The new name for the symbol
1849 |         :return: A WorkspaceEdit containing the changes needed to rename the symbol, or None if rename is not supported
1850 |         """
1851 |         params = RenameParams(
1852 |             textDocument=ls_types.TextDocumentIdentifier(
1853 |                 uri=pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()
1854 |             ),
1855 |             position=ls_types.Position(line=line, character=column),
1856 |             newName=new_name,
1857 |         )
1858 | 
1859 |         return self.server.send.rename(params)
1860 | 
1861 |     def apply_text_edits_to_file(self, relative_path: str, edits: list[ls_types.TextEdit]) -> None:
1862 |         """
1863 |         Apply a list of text edits to a file.
1864 | 
1865 |         :param relative_path: The relative path of the file to edit
1866 |         :param edits: List of TextEdit dictionaries to apply
1867 |         """
1868 |         with self.open_file(relative_path):
1869 |             # Sort edits by position (latest first) to avoid position shifts
1870 |             sorted_edits = sorted(edits, key=lambda e: (e["range"]["start"]["line"], e["range"]["start"]["character"]), reverse=True)
1871 | 
1872 |             for edit in sorted_edits:
1873 |                 start_pos = ls_types.Position(line=edit["range"]["start"]["line"], character=edit["range"]["start"]["character"])
1874 |                 end_pos = ls_types.Position(line=edit["range"]["end"]["line"], character=edit["range"]["end"]["character"])
1875 | 
1876 |                 # Delete the old text and insert the new text
1877 |                 self.delete_text_between_positions(relative_path, start_pos, end_pos)
1878 |                 self.insert_text_at_position(relative_path, start_pos["line"], start_pos["character"], edit["newText"])
1879 | 
1880 |     def start(self) -> "SolidLanguageServer":
1881 |         """
1882 |         Starts the language server process and connects to it. Call shutdown when ready.
1883 | 
1884 |         :return: self for method chaining
1885 |         """
1886 |         log.info(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}")
1887 |         self._start_server_process()
1888 |         return self
1889 | 
1890 |     def stop(self, shutdown_timeout: float = 2.0) -> None:
1891 |         """
1892 |         Stops the language server process.
1893 |         This function never raises an exception (any exceptions during shutdown are logged).
1894 | 
1895 |         :param shutdown_timeout: time, in seconds, to wait for the server to shutdown gracefully before killing it
1896 |         """
1897 |         try:
1898 |             self._shutdown(timeout=shutdown_timeout)
1899 |         except Exception as e:
1900 |             log.warning(f"Exception while shutting down language server: {e}")
1901 | 
1902 |     @property
1903 |     def language_server(self) -> Self:
1904 |         return self
1905 | 
1906 |     @property
1907 |     def handler(self) -> SolidLanguageServerHandler:
1908 |         """Access the underlying language server handler.
1909 | 
1910 |         Useful for advanced operations like sending custom commands
1911 |         or registering notification handlers.
1912 |         """
1913 |         return self.server
1914 | 
1915 |     def is_running(self) -> bool:
1916 |         return self.server.is_running()
1917 | 
```
Page 20/21FirstPrevNextLast