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

# Directory Structure

```
├── .devcontainer
│   └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── issue--bug--performance-problem--question-.md
│   └── workflows
│       ├── codespell.yml
│       ├── docker.yml
│       ├── junie.yml
│       ├── lint_and_docs.yaml
│       ├── publish.yml
│       └── pytest.yml
├── .gitignore
├── .serena
│   ├── memories
│   │   ├── adding_new_language_support_guide.md
│   │   ├── serena_core_concepts_and_architecture.md
│   │   ├── serena_repository_structure.md
│   │   └── suggested_commands.md
│   └── project.yml
├── .vscode
│   └── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── compose.yaml
├── CONTRIBUTING.md
├── docker_build_and_run.sh
├── DOCKER.md
├── Dockerfile
├── docs
│   ├── custom_agent.md
│   └── serena_on_chatgpt.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── public
│   └── .gitignore
├── pyproject.toml
├── README.md
├── resources
│   ├── serena-icons.cdr
│   ├── serena-logo-dark-mode.svg
│   ├── serena-logo.cdr
│   ├── serena-logo.svg
│   └── vscode_sponsor_logo.png
├── roadmap.md
├── scripts
│   ├── agno_agent.py
│   ├── demo_run_tools.py
│   ├── gen_prompt_factory.py
│   ├── mcp_server.py
│   ├── print_mode_context_options.py
│   └── print_tool_overview.py
├── src
│   ├── interprompt
│   │   ├── __init__.py
│   │   ├── .syncCommitId.remote
│   │   ├── .syncCommitId.this
│   │   ├── jinja_template.py
│   │   ├── multilang_prompt.py
│   │   ├── prompt_factory.py
│   │   └── util
│   │       ├── __init__.py
│   │       └── class_decorators.py
│   ├── README.md
│   ├── serena
│   │   ├── __init__.py
│   │   ├── agent.py
│   │   ├── agno.py
│   │   ├── analytics.py
│   │   ├── cli.py
│   │   ├── code_editor.py
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   ├── context_mode.py
│   │   │   └── serena_config.py
│   │   ├── constants.py
│   │   ├── dashboard.py
│   │   ├── generated
│   │   │   └── generated_prompt_factory.py
│   │   ├── gui_log_viewer.py
│   │   ├── mcp.py
│   │   ├── project.py
│   │   ├── prompt_factory.py
│   │   ├── resources
│   │   │   ├── config
│   │   │   │   ├── contexts
│   │   │   │   │   ├── agent.yml
│   │   │   │   │   ├── chatgpt.yml
│   │   │   │   │   ├── codex.yml
│   │   │   │   │   ├── context.template.yml
│   │   │   │   │   ├── desktop-app.yml
│   │   │   │   │   ├── ide-assistant.yml
│   │   │   │   │   └── oaicompat-agent.yml
│   │   │   │   ├── internal_modes
│   │   │   │   │   └── jetbrains.yml
│   │   │   │   ├── modes
│   │   │   │   │   ├── editing.yml
│   │   │   │   │   ├── interactive.yml
│   │   │   │   │   ├── mode.template.yml
│   │   │   │   │   ├── no-onboarding.yml
│   │   │   │   │   ├── onboarding.yml
│   │   │   │   │   ├── one-shot.yml
│   │   │   │   │   └── planning.yml
│   │   │   │   └── prompt_templates
│   │   │   │       ├── simple_tool_outputs.yml
│   │   │   │       └── system_prompt.yml
│   │   │   ├── dashboard
│   │   │   │   ├── dashboard.js
│   │   │   │   ├── index.html
│   │   │   │   ├── jquery.min.js
│   │   │   │   ├── serena-icon-16.png
│   │   │   │   ├── serena-icon-32.png
│   │   │   │   ├── serena-icon-48.png
│   │   │   │   ├── serena-logs-dark-mode.png
│   │   │   │   └── serena-logs.png
│   │   │   ├── project.template.yml
│   │   │   └── serena_config.template.yml
│   │   ├── symbol.py
│   │   ├── text_utils.py
│   │   ├── tools
│   │   │   ├── __init__.py
│   │   │   ├── cmd_tools.py
│   │   │   ├── config_tools.py
│   │   │   ├── file_tools.py
│   │   │   ├── jetbrains_plugin_client.py
│   │   │   ├── jetbrains_tools.py
│   │   │   ├── memory_tools.py
│   │   │   ├── symbol_tools.py
│   │   │   ├── tools_base.py
│   │   │   └── workflow_tools.py
│   │   └── util
│   │       ├── class_decorators.py
│   │       ├── exception.py
│   │       ├── file_system.py
│   │       ├── general.py
│   │       ├── git.py
│   │       ├── inspection.py
│   │       ├── logging.py
│   │       ├── shell.py
│   │       └── thread.py
│   └── solidlsp
│       ├── __init__.py
│       ├── .gitignore
│       ├── language_servers
│       │   ├── al_language_server.py
│       │   ├── bash_language_server.py
│       │   ├── clangd_language_server.py
│       │   ├── clojure_lsp.py
│       │   ├── common.py
│       │   ├── csharp_language_server.py
│       │   ├── dart_language_server.py
│       │   ├── eclipse_jdtls.py
│       │   ├── elixir_tools
│       │   │   ├── __init__.py
│       │   │   ├── elixir_tools.py
│       │   │   └── README.md
│       │   ├── elm_language_server.py
│       │   ├── erlang_language_server.py
│       │   ├── gopls.py
│       │   ├── intelephense.py
│       │   ├── jedi_server.py
│       │   ├── kotlin_language_server.py
│       │   ├── lua_ls.py
│       │   ├── marksman.py
│       │   ├── nixd_ls.py
│       │   ├── omnisharp
│       │   │   ├── initialize_params.json
│       │   │   ├── runtime_dependencies.json
│       │   │   └── workspace_did_change_configuration.json
│       │   ├── omnisharp.py
│       │   ├── perl_language_server.py
│       │   ├── pyright_server.py
│       │   ├── r_language_server.py
│       │   ├── regal_server.py
│       │   ├── ruby_lsp.py
│       │   ├── rust_analyzer.py
│       │   ├── solargraph.py
│       │   ├── sourcekit_lsp.py
│       │   ├── terraform_ls.py
│       │   ├── typescript_language_server.py
│       │   ├── vts_language_server.py
│       │   └── zls.py
│       ├── ls_config.py
│       ├── ls_exceptions.py
│       ├── ls_handler.py
│       ├── ls_logger.py
│       ├── ls_request.py
│       ├── ls_types.py
│       ├── ls_utils.py
│       ├── ls.py
│       ├── lsp_protocol_handler
│       │   ├── lsp_constants.py
│       │   ├── lsp_requests.py
│       │   ├── lsp_types.py
│       │   └── server.py
│       ├── settings.py
│       └── util
│           ├── subprocess_util.py
│           └── zip.py
├── test
│   ├── __init__.py
│   ├── conftest.py
│   ├── resources
│   │   └── repos
│   │       ├── al
│   │       │   └── test_repo
│   │       │       ├── app.json
│   │       │       └── src
│   │       │           ├── Codeunits
│   │       │           │   ├── CustomerMgt.Codeunit.al
│   │       │           │   └── PaymentProcessorImpl.Codeunit.al
│   │       │           ├── Enums
│   │       │           │   └── CustomerType.Enum.al
│   │       │           ├── Interfaces
│   │       │           │   └── IPaymentProcessor.Interface.al
│   │       │           ├── Pages
│   │       │           │   ├── CustomerCard.Page.al
│   │       │           │   └── CustomerList.Page.al
│   │       │           ├── TableExtensions
│   │       │           │   └── Item.TableExt.al
│   │       │           └── Tables
│   │       │               └── Customer.Table.al
│   │       ├── bash
│   │       │   └── test_repo
│   │       │       ├── config.sh
│   │       │       ├── main.sh
│   │       │       └── utils.sh
│   │       ├── clojure
│   │       │   └── test_repo
│   │       │       ├── deps.edn
│   │       │       └── src
│   │       │           └── test_app
│   │       │               ├── core.clj
│   │       │               └── utils.clj
│   │       ├── csharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Models
│   │       │       │   └── Person.cs
│   │       │       ├── Program.cs
│   │       │       ├── serena.sln
│   │       │       └── TestProject.csproj
│   │       ├── dart
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── helper.dart
│   │       │       │   ├── main.dart
│   │       │       │   └── models.dart
│   │       │       └── pubspec.yaml
│   │       ├── elixir
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── examples.ex
│   │       │       │   ├── ignored_dir
│   │       │       │   │   └── ignored_module.ex
│   │       │       │   ├── models.ex
│   │       │       │   ├── services.ex
│   │       │       │   ├── test_repo.ex
│   │       │       │   └── utils.ex
│   │       │       ├── mix.exs
│   │       │       ├── mix.lock
│   │       │       ├── scripts
│   │       │       │   └── build_script.ex
│   │       │       └── test
│   │       │           ├── models_test.exs
│   │       │           └── test_repo_test.exs
│   │       ├── elm
│   │       │   └── test_repo
│   │       │       ├── elm.json
│   │       │       ├── Main.elm
│   │       │       └── Utils.elm
│   │       ├── erlang
│   │       │   └── test_repo
│   │       │       ├── hello.erl
│   │       │       ├── ignored_dir
│   │       │       │   └── ignored_module.erl
│   │       │       ├── include
│   │       │       │   ├── records.hrl
│   │       │       │   └── types.hrl
│   │       │       ├── math_utils.erl
│   │       │       ├── rebar.config
│   │       │       ├── src
│   │       │       │   ├── app.erl
│   │       │       │   ├── models.erl
│   │       │       │   ├── services.erl
│   │       │       │   └── utils.erl
│   │       │       └── test
│   │       │           ├── models_tests.erl
│   │       │           └── utils_tests.erl
│   │       ├── go
│   │       │   └── test_repo
│   │       │       └── main.go
│   │       ├── java
│   │       │   └── test_repo
│   │       │       ├── pom.xml
│   │       │       └── src
│   │       │           └── main
│   │       │               └── java
│   │       │                   └── test_repo
│   │       │                       ├── Main.java
│   │       │                       ├── Model.java
│   │       │                       ├── ModelUser.java
│   │       │                       └── Utils.java
│   │       ├── kotlin
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle.kts
│   │       │       └── src
│   │       │           └── main
│   │       │               └── kotlin
│   │       │                   └── test_repo
│   │       │                       ├── Main.kt
│   │       │                       ├── Model.kt
│   │       │                       ├── ModelUser.kt
│   │       │                       └── Utils.kt
│   │       ├── lua
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── main.lua
│   │       │       ├── src
│   │       │       │   ├── calculator.lua
│   │       │       │   └── utils.lua
│   │       │       └── tests
│   │       │           └── test_calculator.lua
│   │       ├── markdown
│   │       │   └── test_repo
│   │       │       ├── api.md
│   │       │       ├── CONTRIBUTING.md
│   │       │       ├── guide.md
│   │       │       └── README.md
│   │       ├── nix
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── default.nix
│   │       │       ├── flake.nix
│   │       │       ├── lib
│   │       │       │   └── utils.nix
│   │       │       ├── modules
│   │       │       │   └── example.nix
│   │       │       └── scripts
│   │       │           └── hello.sh
│   │       ├── perl
│   │       │   └── test_repo
│   │       │       ├── helper.pl
│   │       │       └── main.pl
│   │       ├── php
│   │       │   └── test_repo
│   │       │       ├── helper.php
│   │       │       ├── index.php
│   │       │       └── simple_var.php
│   │       ├── python
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── custom_test
│   │       │       │   ├── __init__.py
│   │       │       │   └── advanced_features.py
│   │       │       ├── examples
│   │       │       │   ├── __init__.py
│   │       │       │   └── user_management.py
│   │       │       ├── ignore_this_dir_with_postfix
│   │       │       │   └── ignored_module.py
│   │       │       ├── scripts
│   │       │       │   ├── __init__.py
│   │       │       │   └── run_app.py
│   │       │       └── test_repo
│   │       │           ├── __init__.py
│   │       │           ├── complex_types.py
│   │       │           ├── models.py
│   │       │           ├── name_collisions.py
│   │       │           ├── nested_base.py
│   │       │           ├── nested.py
│   │       │           ├── overloaded.py
│   │       │           ├── services.py
│   │       │           ├── utils.py
│   │       │           └── variables.py
│   │       ├── r
│   │       │   └── test_repo
│   │       │       ├── .Rbuildignore
│   │       │       ├── DESCRIPTION
│   │       │       ├── examples
│   │       │       │   └── analysis.R
│   │       │       ├── NAMESPACE
│   │       │       └── R
│   │       │           ├── models.R
│   │       │           └── utils.R
│   │       ├── rego
│   │       │   └── test_repo
│   │       │       ├── policies
│   │       │       │   ├── authz.rego
│   │       │       │   └── validation.rego
│   │       │       └── utils
│   │       │           └── helpers.rego
│   │       ├── ruby
│   │       │   └── test_repo
│   │       │       ├── .solargraph.yml
│   │       │       ├── examples
│   │       │       │   └── user_management.rb
│   │       │       ├── lib.rb
│   │       │       ├── main.rb
│   │       │       ├── models.rb
│   │       │       ├── nested.rb
│   │       │       ├── services.rb
│   │       │       └── variables.rb
│   │       ├── rust
│   │       │   ├── test_repo
│   │       │   │   ├── Cargo.lock
│   │       │   │   ├── Cargo.toml
│   │       │   │   └── src
│   │       │   │       ├── lib.rs
│   │       │   │       └── main.rs
│   │       │   └── test_repo_2024
│   │       │       ├── Cargo.lock
│   │       │       ├── Cargo.toml
│   │       │       └── src
│   │       │           ├── lib.rs
│   │       │           └── main.rs
│   │       ├── swift
│   │       │   └── test_repo
│   │       │       ├── Package.swift
│   │       │       └── src
│   │       │           ├── main.swift
│   │       │           └── utils.swift
│   │       ├── terraform
│   │       │   └── test_repo
│   │       │       ├── data.tf
│   │       │       ├── main.tf
│   │       │       ├── outputs.tf
│   │       │       └── variables.tf
│   │       ├── typescript
│   │       │   └── test_repo
│   │       │       ├── .serena
│   │       │       │   └── project.yml
│   │       │       ├── index.ts
│   │       │       ├── tsconfig.json
│   │       │       └── use_helper.ts
│   │       └── zig
│   │           └── test_repo
│   │               ├── .gitignore
│   │               ├── build.zig
│   │               ├── src
│   │               │   ├── calculator.zig
│   │               │   ├── main.zig
│   │               │   └── math_utils.zig
│   │               └── zls.json
│   ├── serena
│   │   ├── __init__.py
│   │   ├── __snapshots__
│   │   │   └── test_symbol_editing.ambr
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   └── test_serena_config.py
│   │   ├── test_edit_marker.py
│   │   ├── test_mcp.py
│   │   ├── test_serena_agent.py
│   │   ├── test_symbol_editing.py
│   │   ├── test_symbol.py
│   │   ├── test_text_utils.py
│   │   ├── test_tool_parameter_types.py
│   │   └── util
│   │       ├── test_exception.py
│   │       └── test_file_system.py
│   └── solidlsp
│       ├── al
│       │   └── test_al_basic.py
│       ├── bash
│       │   ├── __init__.py
│       │   └── test_bash_basic.py
│       ├── clojure
│       │   ├── __init__.py
│       │   └── test_clojure_basic.py
│       ├── csharp
│       │   └── test_csharp_basic.py
│       ├── dart
│       │   ├── __init__.py
│       │   └── test_dart_basic.py
│       ├── elixir
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_elixir_basic.py
│       │   ├── test_elixir_ignored_dirs.py
│       │   ├── test_elixir_integration.py
│       │   └── test_elixir_symbol_retrieval.py
│       ├── elm
│       │   └── test_elm_basic.py
│       ├── erlang
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_erlang_basic.py
│       │   ├── test_erlang_ignored_dirs.py
│       │   └── test_erlang_symbol_retrieval.py
│       ├── go
│       │   └── test_go_basic.py
│       ├── java
│       │   └── test_java_basic.py
│       ├── kotlin
│       │   └── test_kotlin_basic.py
│       ├── lua
│       │   └── test_lua_basic.py
│       ├── markdown
│       │   ├── __init__.py
│       │   └── test_markdown_basic.py
│       ├── nix
│       │   └── test_nix_basic.py
│       ├── perl
│       │   └── test_perl_basic.py
│       ├── php
│       │   └── test_php_basic.py
│       ├── python
│       │   ├── test_python_basic.py
│       │   ├── test_retrieval_with_ignored_dirs.py
│       │   └── test_symbol_retrieval.py
│       ├── r
│       │   ├── __init__.py
│       │   └── test_r_basic.py
│       ├── rego
│       │   └── test_rego_basic.py
│       ├── ruby
│       │   ├── test_ruby_basic.py
│       │   └── test_ruby_symbol_retrieval.py
│       ├── rust
│       │   ├── test_rust_2024_edition.py
│       │   └── test_rust_basic.py
│       ├── swift
│       │   └── test_swift_basic.py
│       ├── terraform
│       │   └── test_terraform_basic.py
│       ├── typescript
│       │   └── test_typescript_basic.py
│       ├── util
│       │   └── test_zip.py
│       └── zig
│           └── test_zig_basic.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/test/serena/test_symbol_editing.py:
--------------------------------------------------------------------------------

```python
  1 | import logging
  2 | import os
  3 | import shutil
  4 | import sys
  5 | import tempfile
  6 | import time
  7 | from abc import abstractmethod
  8 | from collections.abc import Iterator
  9 | from contextlib import contextmanager
 10 | from dataclasses import dataclass, field
 11 | from difflib import SequenceMatcher
 12 | from pathlib import Path
 13 | from typing import Literal, NamedTuple
 14 | 
 15 | import pytest
 16 | 
 17 | from serena.code_editor import CodeEditor, LanguageServerCodeEditor
 18 | from solidlsp.ls_config import Language
 19 | from src.serena.symbol import LanguageServerSymbolRetriever
 20 | from test.conftest import create_ls, get_repo_path
 21 | 
 22 | pytestmark = pytest.mark.snapshot
 23 | 
 24 | log = logging.getLogger(__name__)
 25 | 
 26 | 
 27 | class LineChange(NamedTuple):
 28 |     """Represents a change to a specific line or range of lines."""
 29 | 
 30 |     operation: Literal["insert", "delete", "replace"]
 31 |     original_start: int
 32 |     original_end: int
 33 |     modified_start: int
 34 |     modified_end: int
 35 |     original_lines: list[str]
 36 |     modified_lines: list[str]
 37 | 
 38 | 
 39 | @dataclass
 40 | class CodeDiff:
 41 |     """
 42 |     Represents the difference between original and modified code.
 43 |     Provides object-oriented access to diff information including line numbers.
 44 |     """
 45 | 
 46 |     relative_path: str
 47 |     original_content: str
 48 |     modified_content: str
 49 |     _line_changes: list[LineChange] = field(init=False)
 50 | 
 51 |     def __post_init__(self) -> None:
 52 |         """Compute the diff using difflib's SequenceMatcher."""
 53 |         original_lines = self.original_content.splitlines(keepends=True)
 54 |         modified_lines = self.modified_content.splitlines(keepends=True)
 55 | 
 56 |         matcher = SequenceMatcher(None, original_lines, modified_lines)
 57 |         self._line_changes = []
 58 | 
 59 |         for tag, orig_start, orig_end, mod_start, mod_end in matcher.get_opcodes():
 60 |             if tag == "equal":
 61 |                 continue
 62 |             if tag == "insert":
 63 |                 self._line_changes.append(
 64 |                     LineChange(
 65 |                         operation="insert",
 66 |                         original_start=orig_start,
 67 |                         original_end=orig_start,
 68 |                         modified_start=mod_start,
 69 |                         modified_end=mod_end,
 70 |                         original_lines=[],
 71 |                         modified_lines=modified_lines[mod_start:mod_end],
 72 |                     )
 73 |                 )
 74 |             elif tag == "delete":
 75 |                 self._line_changes.append(
 76 |                     LineChange(
 77 |                         operation="delete",
 78 |                         original_start=orig_start,
 79 |                         original_end=orig_end,
 80 |                         modified_start=mod_start,
 81 |                         modified_end=mod_start,
 82 |                         original_lines=original_lines[orig_start:orig_end],
 83 |                         modified_lines=[],
 84 |                     )
 85 |                 )
 86 |             elif tag == "replace":
 87 |                 self._line_changes.append(
 88 |                     LineChange(
 89 |                         operation="replace",
 90 |                         original_start=orig_start,
 91 |                         original_end=orig_end,
 92 |                         modified_start=mod_start,
 93 |                         modified_end=mod_end,
 94 |                         original_lines=original_lines[orig_start:orig_end],
 95 |                         modified_lines=modified_lines[mod_start:mod_end],
 96 |                     )
 97 |                 )
 98 | 
 99 |     @property
100 |     def line_changes(self) -> list[LineChange]:
101 |         """Get all line changes in the diff."""
102 |         return self._line_changes
103 | 
104 |     @property
105 |     def has_changes(self) -> bool:
106 |         """Check if there are any changes."""
107 |         return len(self._line_changes) > 0
108 | 
109 |     @property
110 |     def added_lines(self) -> list[tuple[int, str]]:
111 |         """Get all added lines with their line numbers (0-based) in the modified file."""
112 |         result = []
113 |         for change in self._line_changes:
114 |             if change.operation in ("insert", "replace"):
115 |                 for i, line in enumerate(change.modified_lines):
116 |                     result.append((change.modified_start + i, line))
117 |         return result
118 | 
119 |     @property
120 |     def deleted_lines(self) -> list[tuple[int, str]]:
121 |         """Get all deleted lines with their line numbers (0-based) in the original file."""
122 |         result = []
123 |         for change in self._line_changes:
124 |             if change.operation in ("delete", "replace"):
125 |                 for i, line in enumerate(change.original_lines):
126 |                     result.append((change.original_start + i, line))
127 |         return result
128 | 
129 |     @property
130 |     def modified_line_numbers(self) -> list[int]:
131 |         """Get all line numbers (0-based) that were modified in the modified file."""
132 |         line_nums: set[int] = set()
133 |         for change in self._line_changes:
134 |             if change.operation in ("insert", "replace"):
135 |                 line_nums.update(range(change.modified_start, change.modified_end))
136 |         return sorted(line_nums)
137 | 
138 |     @property
139 |     def affected_original_line_numbers(self) -> list[int]:
140 |         """Get all line numbers (0-based) that were affected in the original file."""
141 |         line_nums: set[int] = set()
142 |         for change in self._line_changes:
143 |             if change.operation in ("delete", "replace"):
144 |                 line_nums.update(range(change.original_start, change.original_end))
145 |         return sorted(line_nums)
146 | 
147 |     def get_unified_diff(self, context_lines: int = 3) -> str:
148 |         """Get the unified diff as a string."""
149 |         import difflib
150 | 
151 |         original_lines = self.original_content.splitlines(keepends=True)
152 |         modified_lines = self.modified_content.splitlines(keepends=True)
153 | 
154 |         diff = difflib.unified_diff(
155 |             original_lines, modified_lines, fromfile=f"a/{self.relative_path}", tofile=f"b/{self.relative_path}", n=context_lines
156 |         )
157 |         return "".join(diff)
158 | 
159 |     def get_context_diff(self, context_lines: int = 3) -> str:
160 |         """Get the context diff as a string."""
161 |         import difflib
162 | 
163 |         original_lines = self.original_content.splitlines(keepends=True)
164 |         modified_lines = self.modified_content.splitlines(keepends=True)
165 | 
166 |         diff = difflib.context_diff(
167 |             original_lines, modified_lines, fromfile=f"a/{self.relative_path}", tofile=f"b/{self.relative_path}", n=context_lines
168 |         )
169 |         return "".join(diff)
170 | 
171 | 
172 | class EditingTest:
173 |     def __init__(self, language: Language, rel_path: str):
174 |         """
175 |         :param language: the language
176 |         :param rel_path: the relative path of the edited file
177 |         """
178 |         self.rel_path = rel_path
179 |         self.language = language
180 |         self.original_repo_path = get_repo_path(language)
181 |         self.repo_path: Path | None = None
182 | 
183 |     @contextmanager
184 |     def _setup(self) -> Iterator[LanguageServerSymbolRetriever]:
185 |         """Context manager for setup/teardown with a temporary directory, providing the symbol manager."""
186 |         temp_dir = Path(tempfile.mkdtemp())
187 |         self.repo_path = temp_dir / self.original_repo_path.name
188 |         language_server = None  # Initialize language_server
189 |         try:
190 |             print(f"Copying repo from {self.original_repo_path} to {self.repo_path}")
191 |             shutil.copytree(self.original_repo_path, self.repo_path)
192 |             # prevent deadlock on Windows due to file locks caused by antivirus or some other external software
193 |             # wait for a long time here
194 |             if os.name == "nt":
195 |                 time.sleep(0.1)
196 |             log.info(f"Creating language server for {self.language} {self.rel_path}")
197 |             language_server = create_ls(self.language, str(self.repo_path))
198 |             log.info(f"Starting language server for {self.language} {self.rel_path}")
199 |             language_server.start()
200 |             log.info(f"Language server started for {self.language} {self.rel_path}")
201 |             yield LanguageServerSymbolRetriever(lang_server=language_server)
202 |         finally:
203 |             if language_server is not None and language_server.is_running():
204 |                 log.info(f"Stopping language server for {self.language} {self.rel_path}")
205 |                 language_server.stop()
206 |                 # attempt at trigger of garbage collection
207 |                 language_server = None
208 |                 log.info(f"Language server stopped for {self.language} {self.rel_path}")
209 | 
210 |             # prevent deadlock on Windows due to lingering file locks
211 |             if os.name == "nt":
212 |                 time.sleep(0.1)
213 |             log.info(f"Removing temp directory {temp_dir}")
214 |             shutil.rmtree(temp_dir, ignore_errors=True)
215 |             log.info(f"Temp directory {temp_dir} removed")
216 | 
217 |     def _read_file(self, rel_path: str) -> str:
218 |         """Read the content of a file in the test repository."""
219 |         assert self.repo_path is not None
220 |         file_path = self.repo_path / rel_path
221 |         with open(file_path, encoding="utf-8") as f:
222 |             return f.read()
223 | 
224 |     def run_test(self, content_after_ground_truth: str) -> None:
225 |         with self._setup() as symbol_retriever:
226 |             content_before = self._read_file(self.rel_path)
227 |             code_editor = LanguageServerCodeEditor(symbol_retriever)
228 |             self._apply_edit(code_editor)
229 |             content_after = self._read_file(self.rel_path)
230 |             code_diff = CodeDiff(self.rel_path, original_content=content_before, modified_content=content_after)
231 |             self._test_diff(code_diff, content_after_ground_truth)
232 | 
233 |     @abstractmethod
234 |     def _apply_edit(self, code_editor: CodeEditor) -> None:
235 |         pass
236 | 
237 |     def _test_diff(self, code_diff: CodeDiff, snapshot: str) -> None:
238 |         assert code_diff.modified_content == snapshot
239 | 
240 | 
241 | # Python test file path
242 | PYTHON_TEST_REL_FILE_PATH = os.path.join("test_repo", "variables.py")
243 | 
244 | # TypeScript test file path
245 | TYPESCRIPT_TEST_FILE = "index.ts"
246 | 
247 | 
248 | class DeleteSymbolTest(EditingTest):
249 |     def __init__(self, language: Language, rel_path: str, deleted_symbol: str):
250 |         super().__init__(language, rel_path)
251 |         self.deleted_symbol = deleted_symbol
252 |         self.rel_path = rel_path
253 | 
254 |     def _apply_edit(self, code_editor: CodeEditor) -> None:
255 |         code_editor.delete_symbol(self.deleted_symbol, self.rel_path)
256 | 
257 | 
258 | @pytest.mark.parametrize(
259 |     "test_case",
260 |     [
261 |         pytest.param(
262 |             DeleteSymbolTest(
263 |                 Language.PYTHON,
264 |                 PYTHON_TEST_REL_FILE_PATH,
265 |                 "VariableContainer",
266 |             ),
267 |             marks=pytest.mark.python,
268 |         ),
269 |         pytest.param(
270 |             DeleteSymbolTest(
271 |                 Language.TYPESCRIPT,
272 |                 TYPESCRIPT_TEST_FILE,
273 |                 "DemoClass",
274 |             ),
275 |             marks=pytest.mark.typescript,
276 |         ),
277 |     ],
278 | )
279 | def test_delete_symbol(test_case, snapshot):
280 |     test_case.run_test(content_after_ground_truth=snapshot)
281 | 
282 | 
283 | NEW_PYTHON_FUNCTION = """def new_inserted_function():
284 |     print("This is a new function inserted before another.")"""
285 | 
286 | NEW_PYTHON_CLASS_WITH_LEADING_NEWLINES = """
287 | 
288 | class NewInsertedClass:
289 |     pass
290 | """
291 | 
292 | NEW_PYTHON_CLASS_WITH_TRAILING_NEWLINES = """class NewInsertedClass:
293 |     pass
294 | 
295 | 
296 | """
297 | 
298 | NEW_TYPESCRIPT_FUNCTION = """function newInsertedFunction(): void {
299 |     console.log("This is a new function inserted before another.");
300 | }"""
301 | 
302 | 
303 | NEW_PYTHON_VARIABLE = 'new_module_var = "Inserted after typed_module_var"'
304 | 
305 | NEW_TYPESCRIPT_FUNCTION_AFTER = """function newFunctionAfterClass(): void {
306 |     console.log("This function is after DemoClass.");
307 | }"""
308 | 
309 | 
310 | class InsertInRelToSymbolTest(EditingTest):
311 |     def __init__(
312 |         self, language: Language, rel_path: str, symbol_name: str, new_content: str, mode: Literal["before", "after"] | None = None
313 |     ):
314 |         super().__init__(language, rel_path)
315 |         self.symbol_name = symbol_name
316 |         self.new_content = new_content
317 |         self.mode: Literal["before", "after"] | None = mode
318 | 
319 |     def set_mode(self, mode: Literal["before", "after"]):
320 |         self.mode = mode
321 | 
322 |     def _apply_edit(self, code_editor: CodeEditor) -> None:
323 |         assert self.mode is not None
324 |         if self.mode == "before":
325 |             code_editor.insert_before_symbol(self.symbol_name, self.rel_path, self.new_content)
326 |         elif self.mode == "after":
327 |             code_editor.insert_after_symbol(self.symbol_name, self.rel_path, self.new_content)
328 | 
329 | 
330 | @pytest.mark.parametrize("mode", ["before", "after"])
331 | @pytest.mark.parametrize(
332 |     "test_case",
333 |     [
334 |         pytest.param(
335 |             InsertInRelToSymbolTest(
336 |                 Language.PYTHON,
337 |                 PYTHON_TEST_REL_FILE_PATH,
338 |                 "typed_module_var",
339 |                 NEW_PYTHON_VARIABLE,
340 |             ),
341 |             marks=pytest.mark.python,
342 |         ),
343 |         pytest.param(
344 |             InsertInRelToSymbolTest(
345 |                 Language.PYTHON,
346 |                 PYTHON_TEST_REL_FILE_PATH,
347 |                 "use_module_variables",
348 |                 NEW_PYTHON_FUNCTION,
349 |             ),
350 |             marks=pytest.mark.python,
351 |         ),
352 |         pytest.param(
353 |             InsertInRelToSymbolTest(
354 |                 Language.TYPESCRIPT,
355 |                 TYPESCRIPT_TEST_FILE,
356 |                 "DemoClass",
357 |                 NEW_TYPESCRIPT_FUNCTION_AFTER,
358 |             ),
359 |             marks=pytest.mark.typescript,
360 |         ),
361 |         pytest.param(
362 |             InsertInRelToSymbolTest(
363 |                 Language.TYPESCRIPT,
364 |                 TYPESCRIPT_TEST_FILE,
365 |                 "helperFunction",
366 |                 NEW_TYPESCRIPT_FUNCTION,
367 |             ),
368 |             marks=pytest.mark.typescript,
369 |         ),
370 |     ],
371 | )
372 | def test_insert_in_rel_to_symbol(test_case: InsertInRelToSymbolTest, mode: Literal["before", "after"], snapshot):
373 |     test_case.set_mode(mode)
374 |     test_case.run_test(content_after_ground_truth=snapshot)
375 | 
376 | 
377 | @pytest.mark.python
378 | def test_insert_python_class_before(snapshot):
379 |     InsertInRelToSymbolTest(
380 |         Language.PYTHON,
381 |         PYTHON_TEST_REL_FILE_PATH,
382 |         "VariableDataclass",
383 |         NEW_PYTHON_CLASS_WITH_TRAILING_NEWLINES,
384 |         mode="before",
385 |     ).run_test(snapshot)
386 | 
387 | 
388 | @pytest.mark.python
389 | def test_insert_python_class_after(snapshot):
390 |     InsertInRelToSymbolTest(
391 |         Language.PYTHON,
392 |         PYTHON_TEST_REL_FILE_PATH,
393 |         "VariableDataclass",
394 |         NEW_PYTHON_CLASS_WITH_LEADING_NEWLINES,
395 |         mode="after",
396 |     ).run_test(snapshot)
397 | 
398 | 
399 | PYTHON_REPLACED_BODY = """def modify_instance_var(self):
400 |         # This body has been replaced
401 |         self.instance_var = "Replaced!"
402 |         self.reassignable_instance_var = 999
403 | """
404 | 
405 | TYPESCRIPT_REPLACED_BODY = """function printValue() {
406 |         // This body has been replaced
407 |         console.warn("New value: " + this.value);
408 |     }
409 | """
410 | 
411 | 
412 | class ReplaceBodyTest(EditingTest):
413 |     def __init__(self, language: Language, rel_path: str, symbol_name: str, new_body: str):
414 |         super().__init__(language, rel_path)
415 |         self.symbol_name = symbol_name
416 |         self.new_body = new_body
417 | 
418 |     def _apply_edit(self, code_editor: CodeEditor) -> None:
419 |         code_editor.replace_body(self.symbol_name, self.rel_path, self.new_body)
420 | 
421 | 
422 | @pytest.mark.parametrize(
423 |     "test_case",
424 |     [
425 |         pytest.param(
426 |             ReplaceBodyTest(
427 |                 Language.PYTHON,
428 |                 PYTHON_TEST_REL_FILE_PATH,
429 |                 "VariableContainer/modify_instance_var",
430 |                 PYTHON_REPLACED_BODY,
431 |             ),
432 |             marks=pytest.mark.python,
433 |         ),
434 |         pytest.param(
435 |             ReplaceBodyTest(
436 |                 Language.TYPESCRIPT,
437 |                 TYPESCRIPT_TEST_FILE,
438 |                 "DemoClass/printValue",
439 |                 TYPESCRIPT_REPLACED_BODY,
440 |             ),
441 |             marks=pytest.mark.typescript,
442 |         ),
443 |     ],
444 | )
445 | def test_replace_body(test_case: ReplaceBodyTest, snapshot):
446 |     test_case.run_test(content_after_ground_truth=snapshot)
447 | 
448 | 
449 | NIX_ATTR_REPLACEMENT = """c = 3;"""
450 | 
451 | 
452 | class NixAttrReplacementTest(EditingTest):
453 |     """Test for replacing individual attributes in Nix that should NOT result in double semicolons."""
454 | 
455 |     def __init__(self, language: Language, rel_path: str, symbol_name: str, new_body: str):
456 |         super().__init__(language, rel_path)
457 |         self.symbol_name = symbol_name
458 |         self.new_body = new_body
459 | 
460 |     def _apply_edit(self, code_editor: CodeEditor) -> None:
461 |         code_editor.replace_body(self.symbol_name, self.rel_path, self.new_body)
462 | 
463 | 
464 | @pytest.mark.nix
465 | @pytest.mark.skipif(sys.platform == "win32", reason="nixd language server doesn't run on Windows")
466 | def test_nix_symbol_replacement_no_double_semicolon(snapshot):
467 |     """
468 |     Test that replacing a Nix attribute does not result in double semicolons.
469 | 
470 |     This test exercises the bug where:
471 |     - Original: users.users.example = { isSystemUser = true; group = "example"; description = "Example service user"; };
472 |     - Replacement: c = 3;
473 |     - Bug result would be: c = 3;; (double semicolon)
474 |     - Correct result should be: c = 3; (single semicolon)
475 | 
476 |     The replacement body includes a semicolon, but the language server's range extension
477 |     logic should prevent double semicolons.
478 |     """
479 |     test_case = NixAttrReplacementTest(
480 |         Language.NIX,
481 |         "default.nix",
482 |         "testUser",  # Simple attrset with multiple key-value pairs
483 |         NIX_ATTR_REPLACEMENT,
484 |     )
485 |     test_case.run_test(content_after_ground_truth=snapshot)
486 | 
```

--------------------------------------------------------------------------------
/src/interprompt/multilang_prompt.py:
--------------------------------------------------------------------------------

```python
  1 | import logging
  2 | import os
  3 | from enum import Enum
  4 | from typing import Any, Generic, Literal, TypeVar
  5 | 
  6 | import yaml
  7 | from sensai.util.string import ToStringMixin
  8 | 
  9 | from .jinja_template import JinjaTemplate, ParameterizedTemplateInterface
 10 | 
 11 | log = logging.getLogger(__name__)
 12 | 
 13 | 
 14 | class PromptTemplate(ToStringMixin, ParameterizedTemplateInterface):
 15 |     def __init__(self, name: str, jinja_template_string: str) -> None:
 16 |         self.name = name
 17 |         self._jinja_template = JinjaTemplate(jinja_template_string.strip())
 18 | 
 19 |     def _tostring_exclude_private(self) -> bool:
 20 |         return True
 21 | 
 22 |     def render(self, **params: Any) -> str:
 23 |         return self._jinja_template.render(**params)
 24 | 
 25 |     def get_parameters(self) -> list[str]:
 26 |         return self._jinja_template.get_parameters()
 27 | 
 28 | 
 29 | class PromptList:
 30 |     def __init__(self, items: list[str]) -> None:
 31 |         self.items = [x.strip() for x in items]
 32 | 
 33 |     def to_string(self) -> str:
 34 |         bullet = " * "
 35 |         indent = " " * len(bullet)
 36 |         items = [x.replace("\n", "\n" + indent) for x in self.items]
 37 |         return "\n * ".join(items)
 38 | 
 39 | 
 40 | T = TypeVar("T")
 41 | DEFAULT_LANG_CODE = "default"
 42 | 
 43 | 
 44 | class LanguageFallbackMode(Enum):
 45 |     """
 46 |     Defines what to do if there is no item for the given language.
 47 |     """
 48 | 
 49 |     ANY = "any"
 50 |     """
 51 |     Return the item for any language (the first one found)
 52 |     """
 53 |     EXCEPTION = "exception"
 54 |     """
 55 |     If the requested language is not found, raise an exception
 56 |     """
 57 |     USE_DEFAULT_LANG = "use_default_lang"
 58 |     """
 59 |     If the requested language is not found, use the default language
 60 |     """
 61 | 
 62 | 
 63 | class _MultiLangContainer(Generic[T], ToStringMixin):
 64 |     """
 65 |     A container of items (usually, all having the same semantic meaning) which are associated with different languages.
 66 |     Can also be used for single-language purposes by always using the default language code.
 67 |     """
 68 | 
 69 |     def __init__(self, name: str) -> None:
 70 |         self.name = name
 71 |         self._lang2item: dict[str, T] = {}
 72 |         """Maps language codes to items"""
 73 | 
 74 |     def _tostring_excludes(self) -> list[str]:
 75 |         return ["lang2item"]
 76 | 
 77 |     def _tostring_additional_entries(self) -> dict[str, Any]:
 78 |         return dict(languages=list(self._lang2item.keys()))
 79 | 
 80 |     def get_language_codes(self) -> list[str]:
 81 |         """The language codes for which items are registered in the container."""
 82 |         return list(self._lang2item.keys())
 83 | 
 84 |     def add_item(self, item: T, lang_code: str = DEFAULT_LANG_CODE, allow_overwrite: bool = False) -> None:
 85 |         """Adds an item to the container, representing the same semantic entity as the other items in the container but in a different language.
 86 | 
 87 |         :param item: the item to add
 88 |         :param lang_code: the language shortcode for which to add the item. Use the default for single-language use cases.
 89 |         :param allow_overwrite: if True, allow overwriting an existing entry for the same language
 90 |         """
 91 |         if not allow_overwrite and lang_code in self._lang2item:
 92 |             raise KeyError(f"Item for language '{lang_code}' already registered for name '{self.name}'")
 93 |         self._lang2item[lang_code] = item
 94 | 
 95 |     def has_item(self, lang_code: str = DEFAULT_LANG_CODE) -> bool:
 96 |         return lang_code in self._lang2item
 97 | 
 98 |     def get_item(self, lang: str = DEFAULT_LANG_CODE, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> T:
 99 |         """
100 |         Gets the item for the given language.
101 | 
102 |         :param lang: the language shortcode for which to obtain the prompt template. A default language can be specified.
103 |         :param fallback_mode: defines what to do if there is no item for the given language
104 |         :return: the item
105 |         """
106 |         try:
107 |             return self._lang2item[lang]
108 |         except KeyError as outer_e:
109 |             if fallback_mode == LanguageFallbackMode.EXCEPTION:
110 |                 raise KeyError(f"Item for language '{lang}' not found for name '{self.name}'") from outer_e
111 |             if fallback_mode == LanguageFallbackMode.ANY:
112 |                 try:
113 |                     return next(iter(self._lang2item.values()))
114 |                 except StopIteration as e:
115 |                     raise KeyError(f"No items registered for any language in container '{self.name}'") from e
116 |             if fallback_mode == LanguageFallbackMode.USE_DEFAULT_LANG:
117 |                 try:
118 |                     return self._lang2item[DEFAULT_LANG_CODE]
119 |                 except KeyError as e:
120 |                     raise KeyError(
121 |                         f"Item not found neither for {lang=} nor for the default language '{DEFAULT_LANG_CODE}' in container '{self.name}'"
122 |                     ) from e
123 | 
124 |     def __len__(self) -> int:
125 |         return len(self._lang2item)
126 | 
127 | 
128 | class MultiLangPromptTemplate(ParameterizedTemplateInterface):
129 |     """
130 |     Represents a prompt template with support for multiple languages.
131 |     The parameters of all prompt templates (for all languages) are (must be) the same.
132 |     """
133 | 
134 |     def __init__(self, name: str) -> None:
135 |         self._prompts_container = _MultiLangContainer[PromptTemplate](name)
136 | 
137 |     def __len__(self) -> int:
138 |         return len(self._prompts_container)
139 | 
140 |     @property
141 |     def name(self) -> str:
142 |         return self._prompts_container.name
143 | 
144 |     def add_prompt_template(
145 |         self, prompt_template: PromptTemplate, lang_code: str = DEFAULT_LANG_CODE, allow_overwrite: bool = False
146 |     ) -> None:
147 |         """
148 |         Adds a prompt template for a new language.
149 |         The parameters of all prompt templates (for all languages) are (must be) the same, so if a prompt template is already registered,
150 |         the parameters of the new prompt template should be the same as the existing ones.
151 | 
152 |         :param prompt_template: the prompt template to add
153 |         :param lang_code: the language code for which to add the prompt template. For single-language use cases, you should always use the default language code.
154 |         :param allow_overwrite: whether to allow overwriting an existing entry for the same language
155 |         """
156 |         incoming_parameters = prompt_template.get_parameters()
157 |         if len(self) > 0:
158 |             parameters = self.get_parameters()
159 |             if parameters != incoming_parameters:
160 |                 raise ValueError(
161 |                     f"Cannot add prompt template for language '{lang_code}' to MultiLangPromptTemplate '{self.name}'"
162 |                     f"because the parameters are inconsistent: {parameters} vs {prompt_template.get_parameters()}"
163 |                 )
164 | 
165 |         self._prompts_container.add_item(prompt_template, lang_code, allow_overwrite)
166 | 
167 |     def get_prompt_template(
168 |         self, lang_code: str = DEFAULT_LANG_CODE, fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION
169 |     ) -> PromptTemplate:
170 |         return self._prompts_container.get_item(lang_code, fallback_mode)
171 | 
172 |     def get_parameters(self) -> list[str]:
173 |         if len(self) == 0:
174 |             raise RuntimeError(
175 |                 f"No prompt templates registered for MultiLangPromptTemplate '{self.name}', make sure to register a prompt template before accessing the parameters"
176 |             )
177 |         first_prompt_template = next(iter(self._prompts_container._lang2item.values()))
178 |         return first_prompt_template.get_parameters()
179 | 
180 |     def render(
181 |         self,
182 |         params: dict[str, Any],
183 |         lang_code: str = DEFAULT_LANG_CODE,
184 |         fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION,
185 |     ) -> str:
186 |         prompt_template = self.get_prompt_template(lang_code, fallback_mode)
187 |         return prompt_template.render(**params)
188 | 
189 |     def has_item(self, lang_code: str = DEFAULT_LANG_CODE) -> bool:
190 |         return self._prompts_container.has_item(lang_code)
191 | 
192 | 
193 | class MultiLangPromptList(_MultiLangContainer[PromptList]):
194 |     pass
195 | 
196 | 
197 | class MultiLangPromptCollection:
198 |     """
199 |     Main class for managing a collection of prompt templates and prompt lists, with support for multiple languages.
200 |     All data will be read from the yamls directly contained in the given directory on initialization.
201 |     It is thus assumed that you manage one directory per prompt collection.
202 | 
203 |     The yamls are assumed to be either of the form
204 | 
205 |     ```yaml
206 |     lang: <language_code> # optional, defaults to "default"
207 |     prompts:
208 |       <prompt_name>:
209 |         <prompt_template_string>
210 |       <prompt_list_name>: [<prompt_string_1>, <prompt_string_2>, ...]
211 | 
212 |     ```
213 | 
214 |     When specifying prompt templates for multiple languages, make sure that the Jinja template parameters
215 |     (inferred from the things inside the `{{ }}` in the template strings) are the same for all languages
216 |     (you will get an exception otherwise).
217 | 
218 |     The prompt names must be unique (for the same language) within the collection.
219 |     """
220 | 
221 |     def __init__(self, prompts_dir: str | list[str], fallback_mode: LanguageFallbackMode = LanguageFallbackMode.EXCEPTION) -> None:
222 |         """
223 |         :param prompts_dir: the directory containing the prompt templates and prompt lists.
224 |             If a list is provided, will look for prompt templates in the dirs from left to right
225 |             (first one containing the desired template wins).
226 |         :param fallback_mode: the fallback mode to use when a prompt template or prompt list is not found for the requested language.
227 |             May be reset after initialization.
228 |         """
229 |         self._multi_lang_prompt_templates: dict[str, MultiLangPromptTemplate] = {}
230 |         self._multi_lang_prompt_lists: dict[str, MultiLangPromptList] = {}
231 |         if isinstance(prompts_dir, str):
232 |             prompts_dir = [prompts_dir]
233 | 
234 |         # Add prompts from multiple directories, prioritizing names from the left.
235 |         # If name collisions appear in the first directory, an error is raised (so the first directory should have no
236 |         # internal collisions, this helps in avoiding errors)
237 |         # For all following directories, on a collision the new value will be ignored.
238 |         # This also means that for the following directories, there is no error check on collisions internal to them.
239 |         # We assume that they are correct (i.e., they have no internal collisions).
240 |         first_prompts_dir, fallback_prompt_dirs = prompts_dir[0], prompts_dir[1:]
241 |         self._load_from_disc(first_prompts_dir, on_name_collision="raise")
242 |         for fallback_prompt_dir in fallback_prompt_dirs:
243 |             # already loaded prompts have priority
244 |             self._load_from_disc(fallback_prompt_dir, on_name_collision="skip")
245 | 
246 |         self.fallback_mode = fallback_mode
247 | 
248 |     def _add_prompt_template(
249 |         self,
250 |         name: str,
251 |         template_str: str,
252 |         lang_code: str = DEFAULT_LANG_CODE,
253 |         on_name_collision: Literal["skip", "overwrite", "raise"] = "raise",
254 |     ) -> None:
255 |         """
256 |         :param name: name of the prompt template
257 |         :param template_str: the Jinja template string
258 |         :param lang_code: the language code for which to add the prompt template.
259 |         :param on_name_collision: how to deal with name/lang_code collisions
260 |         """
261 |         allow_overwrite = False
262 |         prompt_template = PromptTemplate(name, template_str)
263 |         mlpt = self._multi_lang_prompt_templates.get(name)
264 |         if mlpt is None:
265 |             mlpt = MultiLangPromptTemplate(name)
266 |             self._multi_lang_prompt_templates[name] = mlpt
267 |         if mlpt.has_item(lang_code):
268 |             if on_name_collision == "raise":
269 |                 raise KeyError(f"Prompt '{name}' for {lang_code} already exists!")
270 |             if on_name_collision == "skip":
271 |                 log.debug(f"Skipping prompt '{name}' since it already exists.")
272 |                 return
273 |             elif on_name_collision == "overwrite":
274 |                 allow_overwrite = True
275 |         mlpt.add_prompt_template(prompt_template, lang_code=lang_code, allow_overwrite=allow_overwrite)
276 | 
277 |     def _add_prompt_list(
278 |         self,
279 |         name: str,
280 |         prompt_list: list[str],
281 |         lang_code: str = DEFAULT_LANG_CODE,
282 |         on_name_collision: Literal["skip", "overwrite", "raise"] = "raise",
283 |     ) -> None:
284 |         """
285 |         :param name: name of the prompt list
286 |         :param prompt_list: a list of prompts
287 |         :param lang_code: the language code for which to add the prompt list.
288 |         :param on_name_collision: how to deal with name/lang_code collisions
289 |         """
290 |         allow_overwrite = False
291 |         multilang_prompt_list = self._multi_lang_prompt_lists.get(name)
292 |         if multilang_prompt_list is None:
293 |             multilang_prompt_list = MultiLangPromptList(name)
294 |             self._multi_lang_prompt_lists[name] = multilang_prompt_list
295 |         if multilang_prompt_list.has_item(lang_code):
296 |             if on_name_collision == "raise":
297 |                 raise KeyError(f"Prompt '{name}' for {lang_code} already exists!")
298 |             if on_name_collision == "skip":
299 |                 log.debug(f"Skipping prompt '{name}' since it already exists.")
300 |                 return
301 |             elif on_name_collision == "overwrite":
302 |                 allow_overwrite = True
303 |         multilang_prompt_list.add_item(PromptList(prompt_list), lang_code=lang_code, allow_overwrite=allow_overwrite)
304 | 
305 |     def _load_from_disc(self, prompts_dir: str, on_name_collision: Literal["skip", "overwrite", "raise"] = "raise") -> None:
306 |         """Loads all prompt templates and prompt lists from yaml files in the given directory.
307 | 
308 |         :param prompts_dir:
309 |         :param on_name_collision: how to deal with name/lang_code collisions
310 |         """
311 |         for fn in os.listdir(prompts_dir):
312 |             if not fn.endswith((".yml", ".yaml")):
313 |                 log.debug(f"Skipping non-YAML file: {fn}")
314 |                 continue
315 |             path = os.path.join(prompts_dir, fn)
316 |             with open(path, encoding="utf-8") as f:
317 |                 data = yaml.safe_load(f)
318 |             try:
319 |                 prompts_data = data["prompts"]
320 |             except KeyError as e:
321 |                 raise KeyError(f"Invalid yaml structure (missing 'prompts' key) in file {path}") from e
322 | 
323 |             lang_code = prompts_data.get("lang", DEFAULT_LANG_CODE)
324 |             # add the data to the collection
325 |             for prompt_name, prompt_template_or_list in prompts_data.items():
326 |                 if isinstance(prompt_template_or_list, list):
327 |                     self._add_prompt_list(prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision)
328 |                 elif isinstance(prompt_template_or_list, str):
329 |                     self._add_prompt_template(
330 |                         prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision
331 |                     )
332 |                 else:
333 |                     raise ValueError(
334 |                         f"Invalid prompt type for {prompt_name} in file {path} (should be str or list): {prompt_template_or_list}"
335 |                     )
336 | 
337 |     def get_prompt_template_names(self) -> list[str]:
338 |         return list(self._multi_lang_prompt_templates.keys())
339 | 
340 |     def get_prompt_list_names(self) -> list[str]:
341 |         return list(self._multi_lang_prompt_lists.keys())
342 | 
343 |     def __len__(self) -> int:
344 |         return len(self._multi_lang_prompt_templates)
345 | 
346 |     def get_multilang_prompt_template(self, prompt_name: str) -> MultiLangPromptTemplate:
347 |         """The MultiLangPromptTemplate object for the given prompt name. For single-language use cases, you should use the `get_prompt_template` method instead."""
348 |         return self._multi_lang_prompt_templates[prompt_name]
349 | 
350 |     def get_multilang_prompt_list(self, prompt_name: str) -> MultiLangPromptList:
351 |         return self._multi_lang_prompt_lists[prompt_name]
352 | 
353 |     def get_prompt_template(
354 |         self,
355 |         prompt_name: str,
356 |         lang_code: str = DEFAULT_LANG_CODE,
357 |     ) -> PromptTemplate:
358 |         """The PromptTemplate object for the given prompt name and language code."""
359 |         return self.get_multilang_prompt_template(prompt_name).get_prompt_template(lang_code=lang_code, fallback_mode=self.fallback_mode)
360 | 
361 |     def get_prompt_template_parameters(self, prompt_name: str) -> list[str]:
362 |         """The parameters of the PromptTemplate object for the given prompt name."""
363 |         return self.get_multilang_prompt_template(prompt_name).get_parameters()
364 | 
365 |     def get_prompt_list(self, prompt_name: str, lang_code: str = DEFAULT_LANG_CODE) -> PromptList:
366 |         """The PromptList object for the given prompt name and language code."""
367 |         return self.get_multilang_prompt_list(prompt_name).get_item(lang_code)
368 | 
369 |     def _has_prompt_list(self, prompt_name: str, lang_code: str = DEFAULT_LANG_CODE) -> bool:
370 |         multi_lang_prompt_list = self._multi_lang_prompt_lists.get(prompt_name)
371 |         if multi_lang_prompt_list is None:
372 |             return False
373 |         return multi_lang_prompt_list.has_item(lang_code)
374 | 
375 |     def _has_prompt_template(self, prompt_name: str, lang_code: str = DEFAULT_LANG_CODE) -> bool:
376 |         multi_lang_prompt_template = self._multi_lang_prompt_templates.get(prompt_name)
377 |         if multi_lang_prompt_template is None:
378 |             return False
379 |         return multi_lang_prompt_template.has_item(lang_code)
380 | 
381 |     def render_prompt_template(
382 |         self,
383 |         prompt_name: str,
384 |         params: dict[str, Any],
385 |         lang_code: str = DEFAULT_LANG_CODE,
386 |     ) -> str:
387 |         """Renders the prompt template for the given prompt name and language code."""
388 |         return self.get_prompt_template(prompt_name, lang_code=lang_code).render(**params)
389 | 
```

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

```python
  1 | """
  2 | Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#.
  3 | """
  4 | 
  5 | import json
  6 | import logging
  7 | import os
  8 | import pathlib
  9 | import threading
 10 | from collections.abc import Iterable
 11 | 
 12 | from overrides import override
 13 | 
 14 | from solidlsp.ls import SolidLanguageServer
 15 | from solidlsp.ls_config import LanguageServerConfig
 16 | from solidlsp.ls_exceptions import SolidLSPException
 17 | from solidlsp.ls_logger import LanguageServerLogger
 18 | from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils
 19 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 20 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 21 | from solidlsp.settings import SolidLSPSettings
 22 | 
 23 | 
 24 | def breadth_first_file_scan(root) -> Iterable[str]:
 25 |     """
 26 |     This function was obtained from https://stackoverflow.com/questions/49654234/is-there-a-breadth-first-search-option-available-in-os-walk-or-equivalent-py
 27 |     It traverses the directory tree in breadth first order.
 28 |     """
 29 |     dirs = [root]
 30 |     # while we has dirs to scan
 31 |     while dirs:
 32 |         next_dirs = []
 33 |         for parent in dirs:
 34 |             # scan each dir
 35 |             for f in os.listdir(parent):
 36 |                 # if there is a dir, then save for next ittr
 37 |                 # if it  is a file then yield it (we'll return later)
 38 |                 ff = os.path.join(parent, f)
 39 |                 if os.path.isdir(ff):
 40 |                     next_dirs.append(ff)
 41 |                 else:
 42 |                     yield ff
 43 | 
 44 |         # once we've done all the current dirs then
 45 |         # we set up the next itter as the child dirs
 46 |         # from the current itter.
 47 |         dirs = next_dirs
 48 | 
 49 | 
 50 | def find_least_depth_sln_file(root_dir) -> str | None:
 51 |     for filename in breadth_first_file_scan(root_dir):
 52 |         if filename.endswith(".sln"):
 53 |             return filename
 54 |     return None
 55 | 
 56 | 
 57 | class OmniSharp(SolidLanguageServer):
 58 |     """
 59 |     Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#.
 60 |     """
 61 | 
 62 |     def __init__(
 63 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 64 |     ):
 65 |         """
 66 |         Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 67 |         """
 68 |         omnisharp_executable_path, dll_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 69 | 
 70 |         slnfilename = find_least_depth_sln_file(repository_root_path)
 71 |         if slnfilename is None:
 72 |             logger.log("No *.sln file found in repository", logging.ERROR)
 73 |             raise SolidLSPException("No SLN file found in repository")
 74 | 
 75 |         cmd = " ".join(
 76 |             [
 77 |                 omnisharp_executable_path,
 78 |                 "-lsp",
 79 |                 "--encoding",
 80 |                 "ascii",
 81 |                 "-z",
 82 |                 "-s",
 83 |                 f'"{slnfilename}"',
 84 |                 "--hostPID",
 85 |                 str(os.getpid()),
 86 |                 "DotNet:enablePackageRestore=false",
 87 |                 "--loglevel",
 88 |                 "trace",
 89 |                 "--plugin",
 90 |                 dll_path,
 91 |                 "FileOptions:SystemExcludeSearchPatterns:0=**/.git",
 92 |                 "FileOptions:SystemExcludeSearchPatterns:1=**/.svn",
 93 |                 "FileOptions:SystemExcludeSearchPatterns:2=**/.hg",
 94 |                 "FileOptions:SystemExcludeSearchPatterns:3=**/CVS",
 95 |                 "FileOptions:SystemExcludeSearchPatterns:4=**/.DS_Store",
 96 |                 "FileOptions:SystemExcludeSearchPatterns:5=**/Thumbs.db",
 97 |                 "RoslynExtensionsOptions:EnableAnalyzersSupport=true",
 98 |                 "FormattingOptions:EnableEditorConfigSupport=true",
 99 |                 "RoslynExtensionsOptions:EnableImportCompletion=true",
100 |                 "Sdk:IncludePrereleases=true",
101 |                 "RoslynExtensionsOptions:AnalyzeOpenDocumentsOnly=true",
102 |                 "formattingOptions:useTabs=false",
103 |                 "formattingOptions:tabSize=4",
104 |                 "formattingOptions:indentationSize=4",
105 |             ]
106 |         )
107 |         super().__init__(
108 |             config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp", solidlsp_settings
109 |         )
110 | 
111 |         self.server_ready = threading.Event()
112 |         self.definition_available = threading.Event()
113 |         self.references_available = threading.Event()
114 | 
115 |     @override
116 |     def is_ignored_dirname(self, dirname: str) -> bool:
117 |         return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"]
118 | 
119 |     @staticmethod
120 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
121 |         """
122 |         Returns the initialize params for the Omnisharp Language Server.
123 |         """
124 |         with open(os.path.join(os.path.dirname(__file__), "omnisharp", "initialize_params.json"), encoding="utf-8") as f:
125 |             d = json.load(f)
126 | 
127 |         del d["_description"]
128 | 
129 |         d["processId"] = os.getpid()
130 |         assert d["rootPath"] == "$rootPath"
131 |         d["rootPath"] = repository_absolute_path
132 | 
133 |         assert d["rootUri"] == "$rootUri"
134 |         d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
135 | 
136 |         assert d["workspaceFolders"][0]["uri"] == "$uri"
137 |         d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
138 | 
139 |         assert d["workspaceFolders"][0]["name"] == "$name"
140 |         d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
141 | 
142 |         return d
143 | 
144 |     @classmethod
145 |     def _setup_runtime_dependencies(
146 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
147 |     ) -> tuple[str, str]:
148 |         """
149 |         Setup runtime dependencies for OmniSharp.
150 |         """
151 |         platform_id = PlatformUtils.get_platform_id()
152 |         dotnet_version = PlatformUtils.get_dotnet_version()
153 | 
154 |         with open(os.path.join(os.path.dirname(__file__), "omnisharp", "runtime_dependencies.json"), encoding="utf-8") as f:
155 |             d = json.load(f)
156 |             del d["_description"]
157 | 
158 |         assert platform_id in [
159 |             PlatformId.LINUX_x64,
160 |             PlatformId.WIN_x64,
161 |         ], f"Only linux-x64 and win-x64 platform is supported at the moment but got {platform_id=}"
162 |         assert dotnet_version in [
163 |             DotnetVersion.V6,
164 |             DotnetVersion.V7,
165 |             DotnetVersion.V8,
166 |             DotnetVersion.V9,
167 |         ], f"Only dotnet version 6-9 are supported at the moment but got {dotnet_version=}"
168 | 
169 |         # TODO: Do away with this assumption
170 |         # Currently, runtime binaries are not available for .Net 7 and .Net 8. Hence, we assume .Net 6 runtime binaries to be compatible with .Net 7, .Net 8
171 |         if dotnet_version in [DotnetVersion.V7, DotnetVersion.V8, DotnetVersion.V9]:
172 |             dotnet_version = DotnetVersion.V6
173 | 
174 |         runtime_dependencies = d["runtimeDependencies"]
175 |         runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value]
176 |         runtime_dependencies = [
177 |             dependency
178 |             for dependency in runtime_dependencies
179 |             if "dotnet_version" not in dependency or dependency["dotnet_version"] == dotnet_version.value
180 |         ]
181 |         assert len(runtime_dependencies) == 2
182 |         runtime_dependencies = {
183 |             runtime_dependencies[0]["id"]: runtime_dependencies[0],
184 |             runtime_dependencies[1]["id"]: runtime_dependencies[1],
185 |         }
186 | 
187 |         assert "OmniSharp" in runtime_dependencies
188 |         assert "RazorOmnisharp" in runtime_dependencies
189 | 
190 |         omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "OmniSharp")
191 |         if not os.path.exists(omnisharp_ls_dir):
192 |             os.makedirs(omnisharp_ls_dir)
193 |             FileUtils.download_and_extract_archive(logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip")
194 |         omnisharp_executable_path = os.path.join(omnisharp_ls_dir, runtime_dependencies["OmniSharp"]["binaryName"])
195 |         assert os.path.exists(omnisharp_executable_path)
196 |         os.chmod(omnisharp_executable_path, 0o755)
197 | 
198 |         razor_omnisharp_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "RazorOmnisharp")
199 |         if not os.path.exists(razor_omnisharp_ls_dir):
200 |             os.makedirs(razor_omnisharp_ls_dir)
201 |             FileUtils.download_and_extract_archive(logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip")
202 |         razor_omnisharp_dll_path = os.path.join(razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"])
203 |         assert os.path.exists(razor_omnisharp_dll_path)
204 | 
205 |         return omnisharp_executable_path, razor_omnisharp_dll_path
206 | 
207 |     def _start_server(self):
208 |         """
209 |         Starts the Omnisharp Language Server
210 |         """
211 | 
212 |         def register_capability_handler(params):
213 |             assert "registrations" in params
214 |             for registration in params["registrations"]:
215 |                 if registration["method"] == "textDocument/definition":
216 |                     self.definition_available.set()
217 |                 if registration["method"] == "textDocument/references":
218 |                     self.references_available.set()
219 |                 if registration["method"] == "textDocument/completion":
220 |                     self.completions_available.set()
221 | 
222 |         def lang_status_handler(params):
223 |             # TODO: Should we wait for
224 |             # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
225 |             # Before proceeding?
226 |             # if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
227 |             #     self.service_ready_event.set()
228 |             pass
229 | 
230 |         def execute_client_command_handler(params):
231 |             return []
232 | 
233 |         def do_nothing(params):
234 |             return
235 | 
236 |         def check_experimental_status(params):
237 |             if params["quiescent"] is True:
238 |                 self.server_ready.set()
239 | 
240 |         def workspace_configuration_handler(params):
241 |             # TODO: We do not know the appropriate way to handle this request. Should ideally contact the OmniSharp dev team
242 |             return [
243 |                 {
244 |                     "RoslynExtensionsOptions": {
245 |                         "EnableDecompilationSupport": False,
246 |                         "EnableAnalyzersSupport": True,
247 |                         "EnableImportCompletion": True,
248 |                         "EnableAsyncCompletion": False,
249 |                         "DocumentAnalysisTimeoutMs": 30000,
250 |                         "DiagnosticWorkersThreadCount": 18,
251 |                         "AnalyzeOpenDocumentsOnly": True,
252 |                         "InlayHintsOptions": {
253 |                             "EnableForParameters": False,
254 |                             "ForLiteralParameters": False,
255 |                             "ForIndexerParameters": False,
256 |                             "ForObjectCreationParameters": False,
257 |                             "ForOtherParameters": False,
258 |                             "SuppressForParametersThatDifferOnlyBySuffix": False,
259 |                             "SuppressForParametersThatMatchMethodIntent": False,
260 |                             "SuppressForParametersThatMatchArgumentName": False,
261 |                             "EnableForTypes": False,
262 |                             "ForImplicitVariableTypes": False,
263 |                             "ForLambdaParameterTypes": False,
264 |                             "ForImplicitObjectCreation": False,
265 |                         },
266 |                         "LocationPaths": None,
267 |                     },
268 |                     "FormattingOptions": {
269 |                         "OrganizeImports": False,
270 |                         "EnableEditorConfigSupport": True,
271 |                         "NewLine": "\n",
272 |                         "UseTabs": False,
273 |                         "TabSize": 4,
274 |                         "IndentationSize": 4,
275 |                         "SpacingAfterMethodDeclarationName": False,
276 |                         "SeparateImportDirectiveGroups": False,
277 |                         "SpaceWithinMethodDeclarationParenthesis": False,
278 |                         "SpaceBetweenEmptyMethodDeclarationParentheses": False,
279 |                         "SpaceAfterMethodCallName": False,
280 |                         "SpaceWithinMethodCallParentheses": False,
281 |                         "SpaceBetweenEmptyMethodCallParentheses": False,
282 |                         "SpaceAfterControlFlowStatementKeyword": True,
283 |                         "SpaceWithinExpressionParentheses": False,
284 |                         "SpaceWithinCastParentheses": False,
285 |                         "SpaceWithinOtherParentheses": False,
286 |                         "SpaceAfterCast": False,
287 |                         "SpaceBeforeOpenSquareBracket": False,
288 |                         "SpaceBetweenEmptySquareBrackets": False,
289 |                         "SpaceWithinSquareBrackets": False,
290 |                         "SpaceAfterColonInBaseTypeDeclaration": True,
291 |                         "SpaceAfterComma": True,
292 |                         "SpaceAfterDot": False,
293 |                         "SpaceAfterSemicolonsInForStatement": True,
294 |                         "SpaceBeforeColonInBaseTypeDeclaration": True,
295 |                         "SpaceBeforeComma": False,
296 |                         "SpaceBeforeDot": False,
297 |                         "SpaceBeforeSemicolonsInForStatement": False,
298 |                         "SpacingAroundBinaryOperator": "single",
299 |                         "IndentBraces": False,
300 |                         "IndentBlock": True,
301 |                         "IndentSwitchSection": True,
302 |                         "IndentSwitchCaseSection": True,
303 |                         "IndentSwitchCaseSectionWhenBlock": True,
304 |                         "LabelPositioning": "oneLess",
305 |                         "WrappingPreserveSingleLine": True,
306 |                         "WrappingKeepStatementsOnSingleLine": True,
307 |                         "NewLinesForBracesInTypes": True,
308 |                         "NewLinesForBracesInMethods": True,
309 |                         "NewLinesForBracesInProperties": True,
310 |                         "NewLinesForBracesInAccessors": True,
311 |                         "NewLinesForBracesInAnonymousMethods": True,
312 |                         "NewLinesForBracesInControlBlocks": True,
313 |                         "NewLinesForBracesInAnonymousTypes": True,
314 |                         "NewLinesForBracesInObjectCollectionArrayInitializers": True,
315 |                         "NewLinesForBracesInLambdaExpressionBody": True,
316 |                         "NewLineForElse": True,
317 |                         "NewLineForCatch": True,
318 |                         "NewLineForFinally": True,
319 |                         "NewLineForMembersInObjectInit": True,
320 |                         "NewLineForMembersInAnonymousTypes": True,
321 |                         "NewLineForClausesInQuery": True,
322 |                     },
323 |                     "FileOptions": {
324 |                         "SystemExcludeSearchPatterns": [
325 |                             "**/node_modules/**/*",
326 |                             "**/bin/**/*",
327 |                             "**/obj/**/*",
328 |                             "**/.git/**/*",
329 |                             "**/.git",
330 |                             "**/.svn",
331 |                             "**/.hg",
332 |                             "**/CVS",
333 |                             "**/.DS_Store",
334 |                             "**/Thumbs.db",
335 |                         ],
336 |                         "ExcludeSearchPatterns": [],
337 |                     },
338 |                     "RenameOptions": {
339 |                         "RenameOverloads": False,
340 |                         "RenameInStrings": False,
341 |                         "RenameInComments": False,
342 |                     },
343 |                     "ImplementTypeOptions": {
344 |                         "InsertionBehavior": 0,
345 |                         "PropertyGenerationBehavior": 0,
346 |                     },
347 |                     "DotNetCliOptions": {"LocationPaths": None},
348 |                     "Plugins": {"LocationPaths": None},
349 |                 }
350 |             ]
351 | 
352 |         self.server.on_request("client/registerCapability", register_capability_handler)
353 |         self.server.on_notification("language/status", lang_status_handler)
354 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
355 |         self.server.on_notification("$/progress", do_nothing)
356 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
357 |         self.server.on_notification("language/actionableNotification", do_nothing)
358 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
359 |         self.server.on_request("workspace/configuration", workspace_configuration_handler)
360 | 
361 |         self.logger.log("Starting OmniSharp server process", logging.INFO)
362 |         self.server.start()
363 |         initialize_params = self._get_initialize_params(self.repository_root_path)
364 | 
365 |         self.logger.log(
366 |             "Sending initialize request from LSP client to LSP server and awaiting response",
367 |             logging.INFO,
368 |         )
369 |         init_response = self.server.send.initialize(initialize_params)
370 |         self.server.notify.initialized({})
371 |         with open(os.path.join(os.path.dirname(__file__), "omnisharp", "workspace_did_change_configuration.json"), encoding="utf-8") as f:
372 |             self.server.notify.workspace_did_change_configuration({"settings": json.load(f)})
373 |         assert "capabilities" in init_response
374 |         if "definitionProvider" in init_response["capabilities"] and init_response["capabilities"]["definitionProvider"]:
375 |             self.definition_available.set()
376 |         if "referencesProvider" in init_response["capabilities"] and init_response["capabilities"]["referencesProvider"]:
377 |             self.references_available.set()
378 | 
379 |         self.definition_available.wait()
380 |         self.references_available.wait()
381 | 
```

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

```json
  1 | {
  2 |     "_description": "Used to download the runtime dependencies for running OmniSharp. Obtained from https://github.com/dotnet/vscode-csharp/blob/main/package.json",
  3 |     "runtimeDependencies": [
  4 |         {
  5 |             "id": "OmniSharp",
  6 |             "description": "OmniSharp for Windows (.NET 4 / x86)",
  7 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x86-1.39.10.zip",
  8 |             "installPath": ".omnisharp/1.39.10",
  9 |             "platforms": [
 10 |                 "win32"
 11 |             ],
 12 |             "architectures": [
 13 |                 "x86"
 14 |             ],
 15 |             "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe",
 16 |             "platformId": "win-x86",
 17 |             "isFramework": true,
 18 |             "integrity": "C81CE2099AD494EF63F9D88FAA70D55A68CF175810F944526FF94AAC7A5109F9",
 19 |             "dotnet_version": "4",
 20 |             "binaryName": "OmniSharp.exe"
 21 |         },
 22 |         {
 23 |             "id": "OmniSharp",
 24 |             "description": "OmniSharp for Windows (.NET 6 / x86)",
 25 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x86-net6.0-1.39.10.zip",
 26 |             "installPath": ".omnisharp/1.39.10-net6.0",
 27 |             "platforms": [
 28 |                 "win32"
 29 |             ],
 30 |             "architectures": [
 31 |                 "x86"
 32 |             ],
 33 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
 34 |             "platformId": "win-x86",
 35 |             "isFramework": false,
 36 |             "integrity": "B7E62415CFC3DAC2154AC636C5BF0FB4B2C9BBF11B5A1FBF72381DDDED59791E",
 37 |             "dotnet_version": "6",
 38 |             "binaryName": "OmniSharp.exe"
 39 |         },
 40 |         {
 41 |             "id": "OmniSharp",
 42 |             "description": "OmniSharp for Windows (.NET 4 / x64)",
 43 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x64-1.39.10.zip",
 44 |             "installPath": ".omnisharp/1.39.10",
 45 |             "platforms": [
 46 |                 "win32"
 47 |             ],
 48 |             "architectures": [
 49 |                 "x86_64"
 50 |             ],
 51 |             "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe",
 52 |             "platformId": "win-x64",
 53 |             "isFramework": true,
 54 |             "integrity": "BE0ED10AACEA17E14B78BD0D887DE5935D4ECA3712192A701F3F2100CA3C8B6E",
 55 |             "dotnet_version": "4",
 56 |             "binaryName": "OmniSharp.exe"
 57 |         },
 58 |         {
 59 |             "id": "OmniSharp",
 60 |             "description": "OmniSharp for Windows (.NET 6 / x64)",
 61 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x64-net6.0-1.39.10.zip",
 62 |             "installPath": ".omnisharp/1.39.10-net6.0",
 63 |             "platforms": [
 64 |                 "win32"
 65 |             ],
 66 |             "architectures": [
 67 |                 "x86_64"
 68 |             ],
 69 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
 70 |             "platformId": "win-x64",
 71 |             "isFramework": false,
 72 |             "integrity": "A73327395E7EF92C1D8E307055463DA412662C03F077ECC743462FD2760BB537",
 73 |             "dotnet_version": "6",
 74 |             "binaryName": "OmniSharp.exe"
 75 |         },
 76 |         {
 77 |             "id": "OmniSharp",
 78 |             "description": "OmniSharp for Windows (.NET 4 / arm64)",
 79 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-arm64-1.39.10.zip",
 80 |             "installPath": ".omnisharp/1.39.10",
 81 |             "platforms": [
 82 |                 "win32"
 83 |             ],
 84 |             "architectures": [
 85 |                 "arm64"
 86 |             ],
 87 |             "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe",
 88 |             "platformId": "win-arm64",
 89 |             "isFramework": true,
 90 |             "integrity": "32FA0067B0639F87760CD1A769B16E6A53588C137C4D31661836CA4FB28D3DD6",
 91 |             "dotnet_version": "4",
 92 |             "binaryName": "OmniSharp.exe"
 93 |         },
 94 |         {
 95 |             "id": "OmniSharp",
 96 |             "description": "OmniSharp for Windows (.NET 6 / arm64)",
 97 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-arm64-net6.0-1.39.10.zip",
 98 |             "installPath": ".omnisharp/1.39.10-net6.0",
 99 |             "platforms": [
100 |                 "win32"
101 |             ],
102 |             "architectures": [
103 |                 "arm64"
104 |             ],
105 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
106 |             "platformId": "win-arm64",
107 |             "isFramework": false,
108 |             "integrity": "433F9B360CAA7B4DDD85C604D5C5542C1A718BCF2E71B2BCFC7526E6D41F4E8F",
109 |             "dotnet_version": "6",
110 |             "binaryName": "OmniSharp.exe"
111 |         },
112 |         {
113 |             "id": "OmniSharp",
114 |             "description": "OmniSharp for OSX (Mono / x64)",
115 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-1.39.10.zip",
116 |             "installPath": ".omnisharp/1.39.10",
117 |             "platforms": [
118 |                 "darwin"
119 |             ],
120 |             "architectures": [
121 |                 "x86_64",
122 |                 "arm64"
123 |             ],
124 |             "binaries": [
125 |                 "./mono.osx",
126 |                 "./run"
127 |             ],
128 |             "installTestPath": "./.omnisharp/1.39.10/run",
129 |             "platformId": "osx",
130 |             "isFramework": true,
131 |             "integrity": "2CC42F0EC7C30CFA8858501D12ECB6FB685A1FCFB8ECB35698A4B12406551968",
132 |             "dotnet_version": "mono"
133 |         },
134 |         {
135 |             "id": "OmniSharp",
136 |             "description": "OmniSharp for OSX (.NET 6 / x64)",
137 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-x64-net6.0-1.39.10.zip",
138 |             "installPath": ".omnisharp/1.39.10-net6.0",
139 |             "platforms": [
140 |                 "darwin"
141 |             ],
142 |             "architectures": [
143 |                 "x86_64"
144 |             ],
145 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
146 |             "platformId": "osx-x64",
147 |             "isFramework": false,
148 |             "integrity": "C9D6E9F2C839A66A7283AE6A9EC545EE049B48EB230D33E91A6322CB67FF9D97",
149 |             "dotnet_version": "6"
150 |         },
151 |         {
152 |             "id": "OmniSharp",
153 |             "description": "OmniSharp for OSX (.NET 6 / arm64)",
154 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-arm64-net6.0-1.39.10.zip",
155 |             "installPath": ".omnisharp/1.39.10-net6.0",
156 |             "platforms": [
157 |                 "darwin"
158 |             ],
159 |             "architectures": [
160 |                 "arm64"
161 |             ],
162 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
163 |             "platformId": "osx-arm64",
164 |             "isFramework": false,
165 |             "integrity": "851350F52F83E3BAD5A92D113E4B9882FCD1DEB16AA84FF94B6F2CEE3C70051E",
166 |             "dotnet_version": "6"
167 |         },
168 |         {
169 |             "id": "OmniSharp",
170 |             "description": "OmniSharp for Linux (Mono / x86)",
171 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x86-1.39.10.zip",
172 |             "installPath": ".omnisharp/1.39.10",
173 |             "platforms": [
174 |                 "linux"
175 |             ],
176 |             "architectures": [
177 |                 "x86",
178 |                 "i686"
179 |             ],
180 |             "binaries": [
181 |                 "./mono.linux-x86",
182 |                 "./run"
183 |             ],
184 |             "installTestPath": "./.omnisharp/1.39.10/run",
185 |             "platformId": "linux-x86",
186 |             "isFramework": true,
187 |             "integrity": "474B1CDBAE64CFEC655FB6B0659BCE481023C48274441C72991E67B6E13E56A1",
188 |             "dotnet_version": "mono"
189 |         },
190 |         {
191 |             "id": "OmniSharp",
192 |             "description": "OmniSharp for Linux (Mono / x64)",
193 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x64-1.39.10.zip",
194 |             "installPath": ".omnisharp/1.39.10",
195 |             "platforms": [
196 |                 "linux"
197 |             ],
198 |             "architectures": [
199 |                 "x86_64"
200 |             ],
201 |             "binaries": [
202 |                 "./mono.linux-x86_64",
203 |                 "./run"
204 |             ],
205 |             "installTestPath": "./.omnisharp/1.39.10/run",
206 |             "platformId": "linux-x64",
207 |             "isFramework": true,
208 |             "integrity": "FB4CAA47343265100349375D79DBCCE1868950CED675CB07FCBE8462EDBCDD37",
209 |             "dotnet_version": "mono"
210 |         },
211 |         {
212 |             "id": "OmniSharp",
213 |             "description": "OmniSharp for Linux (.NET 6 / x64)",
214 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x64-net6.0-1.39.10.zip",
215 |             "installPath": ".omnisharp/1.39.10-net6.0",
216 |             "platforms": [
217 |                 "linux"
218 |             ],
219 |             "architectures": [
220 |                 "x86_64"
221 |             ],
222 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
223 |             "platformId": "linux-x64",
224 |             "isFramework": false,
225 |             "integrity": "0926D3BEA060BF4373356B2FC0A68C10D0DE1B1150100B551BA5932814CE51E2",
226 |             "dotnet_version": "6",
227 |             "binaryName": "OmniSharp"
228 |         },
229 |         {
230 |             "id": "OmniSharp",
231 |             "description": "OmniSharp for Linux (Mono / arm64)",
232 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-arm64-1.39.10.zip",
233 |             "installPath": ".omnisharp/1.39.10",
234 |             "platforms": [
235 |                 "linux"
236 |             ],
237 |             "architectures": [
238 |                 "arm64"
239 |             ],
240 |             "binaries": [
241 |                 "./mono.linux-arm64",
242 |                 "./run"
243 |             ],
244 |             "installTestPath": "./.omnisharp/1.39.10/run",
245 |             "platformId": "linux-arm64",
246 |             "isFramework": true,
247 |             "integrity": "478F3594DFD0167E9A56E36F0364A86C73F8132A3E7EA916CA1419EFE141D2CC",
248 |             "dotnet_version": "mono"
249 |         },
250 |         {
251 |             "id": "OmniSharp",
252 |             "description": "OmniSharp for Linux (.NET 6 / arm64)",
253 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-arm64-net6.0-1.39.10.zip",
254 |             "installPath": ".omnisharp/1.39.10-net6.0",
255 |             "platforms": [
256 |                 "linux"
257 |             ],
258 |             "architectures": [
259 |                 "arm64"
260 |             ],
261 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
262 |             "platformId": "linux-arm64",
263 |             "isFramework": false,
264 |             "integrity": "6FB6A572043A74220A92F6C19C7BB0C3743321C7563A815FD2702EF4FA7D688E",
265 |             "dotnet_version": "6"
266 |         },
267 |         {
268 |             "id": "OmniSharp",
269 |             "description": "OmniSharp for Linux musl (.NET 6 / x64)",
270 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-musl-x64-net6.0-1.39.10.zip",
271 |             "installPath": ".omnisharp/1.39.10-net6.0",
272 |             "platforms": [
273 |                 "linux-musl"
274 |             ],
275 |             "architectures": [
276 |                 "x86_64"
277 |             ],
278 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
279 |             "platformId": "linux-musl-x64",
280 |             "isFramework": false,
281 |             "integrity": "6BFDA3AD11DBB0C6514B86ECC3E1597CC41C6E309B7575F7C599E07D9E2AE610",
282 |             "dotnet_version": "6"
283 |         },
284 |         {
285 |             "id": "OmniSharp",
286 |             "description": "OmniSharp for Linux musl (.NET 6 / arm64)",
287 |             "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-musl-arm64-net6.0-1.39.10.zip",
288 |             "installPath": ".omnisharp/1.39.10-net6.0",
289 |             "platforms": [
290 |                 "linux-musl"
291 |             ],
292 |             "architectures": [
293 |                 "arm64"
294 |             ],
295 |             "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll",
296 |             "platformId": "linux-musl-arm64",
297 |             "isFramework": false,
298 |             "integrity": "DA63619EA024EB9BBF6DB5A85C6150CAB5C0BD554544A3596ED1B17F926D6875",
299 |             "dotnet_version": "6"
300 |         },
301 |         {
302 |             "id": "RazorOmnisharp",
303 |             "description": "Razor Language Server for OmniSharp (Windows / x64)",
304 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/8d42e62ea4051381c219b3e31bc4eced/razorlanguageserver-win-x64-7.0.0-preview.23363.1.zip",
305 |             "installPath": ".razoromnisharp",
306 |             "platforms": [
307 |                 "win32"
308 |             ],
309 |             "architectures": [
310 |                 "x86_64"
311 |             ],
312 |             "platformId": "win-x64",
313 |             "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll"
314 |         },
315 |         {
316 |             "id": "RazorOmnisharp",
317 |             "description": "Razor Language Server for OmniSharp (Windows / x86)",
318 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/e440c4f3a4a96334fe177513935fa010/razorlanguageserver-win-x86-7.0.0-preview.23363.1.zip",
319 |             "installPath": ".razoromnisharp",
320 |             "platforms": [
321 |                 "win32"
322 |             ],
323 |             "architectures": [
324 |                 "x86"
325 |             ],
326 |             "platformId": "win-x86",
327 |             "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll"
328 |         },
329 |         {
330 |             "id": "RazorOmnisharp",
331 |             "description": "Razor Language Server for OmniSharp (Windows / ARM64)",
332 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/4ef26e45cf32fe8d51c0e7dd21f1fef6/razorlanguageserver-win-arm64-7.0.0-preview.23363.1.zip",
333 |             "installPath": ".razoromnisharp",
334 |             "platforms": [
335 |                 "win32"
336 |             ],
337 |             "architectures": [
338 |                 "arm64"
339 |             ],
340 |             "platformId": "win-arm64",
341 |             "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll"
342 |         },
343 |         {
344 |             "id": "RazorOmnisharp",
345 |             "description": "Razor Language Server for OmniSharp (Linux / x64)",
346 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/6d4e23a3c7cf0465743950a39515a716/razorlanguageserver-linux-x64-7.0.0-preview.23363.1.zip",
347 |             "installPath": ".razoromnisharp",
348 |             "platforms": [
349 |                 "linux"
350 |             ],
351 |             "architectures": [
352 |                 "x86_64"
353 |             ],
354 |             "binaries": [
355 |                 "./rzls"
356 |             ],
357 |             "platformId": "linux-x64",
358 |             "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll"
359 |         },
360 |         {
361 |             "id": "RazorOmnisharp",
362 |             "description": "Razor Language Server for OmniSharp (Linux ARM64)",
363 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/85deebd44647ebf65724cc291d722283/razorlanguageserver-linux-arm64-7.0.0-preview.23363.1.zip",
364 |             "installPath": ".razoromnisharp",
365 |             "platforms": [
366 |                 "linux"
367 |             ],
368 |             "architectures": [
369 |                 "arm64"
370 |             ],
371 |             "binaries": [
372 |                 "./rzls"
373 |             ],
374 |             "platformId": "linux-arm64"
375 |         },
376 |         {
377 |             "id": "RazorOmnisharp",
378 |             "description": "Razor Language Server for OmniSharp (Linux musl / x64)",
379 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/4f0caa94ae182785655efb15eafcef23/razorlanguageserver-linux-musl-x64-7.0.0-preview.23363.1.zip",
380 |             "installPath": ".razoromnisharp",
381 |             "platforms": [
382 |                 "linux-musl"
383 |             ],
384 |             "architectures": [
385 |                 "x86_64"
386 |             ],
387 |             "binaries": [
388 |                 "./rzls"
389 |             ],
390 |             "platformId": "linux-musl-x64"
391 |         },
392 |         {
393 |             "id": "RazorOmnisharp",
394 |             "description": "Razor Language Server for OmniSharp (Linux musl ARM64)",
395 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/0a24828206a6f3b4bc743d058ef88ce7/razorlanguageserver-linux-musl-arm64-7.0.0-preview.23363.1.zip",
396 |             "installPath": ".razoromnisharp",
397 |             "platforms": [
398 |                 "linux-musl"
399 |             ],
400 |             "architectures": [
401 |                 "arm64"
402 |             ],
403 |             "binaries": [
404 |                 "./rzls"
405 |             ],
406 |             "platformId": "linux-musl-arm64"
407 |         },
408 |         {
409 |             "id": "RazorOmnisharp",
410 |             "description": "Razor Language Server for OmniSharp (macOS / x64)",
411 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/2afcafaf41082989efcc10405abb9314/razorlanguageserver-osx-x64-7.0.0-preview.23363.1.zip",
412 |             "installPath": ".razoromnisharp",
413 |             "platforms": [
414 |                 "darwin"
415 |             ],
416 |             "architectures": [
417 |                 "x86_64"
418 |             ],
419 |             "binaries": [
420 |                 "./rzls"
421 |             ],
422 |             "platformId": "osx-x64"
423 |         },
424 |         {
425 |             "id": "RazorOmnisharp",
426 |             "description": "Razor Language Server for OmniSharp (macOS ARM64)",
427 |             "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/8bf2ed2f00d481a5987e3eb5165afddd/razorlanguageserver-osx-arm64-7.0.0-preview.23363.1.zip",
428 |             "installPath": ".razoromnisharp",
429 |             "platforms": [
430 |                 "darwin"
431 |             ],
432 |             "architectures": [
433 |                 "arm64"
434 |             ],
435 |             "binaries": [
436 |                 "./rzls"
437 |             ],
438 |             "platformId": "osx-arm64"
439 |         }
440 |     ]
441 | }
```

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

```python
  1 | import logging
  2 | import os
  3 | import pathlib
  4 | import subprocess
  5 | import threading
  6 | import time
  7 | 
  8 | from overrides import override
  9 | 
 10 | from solidlsp import ls_types
 11 | from solidlsp.ls import SolidLanguageServer
 12 | from solidlsp.ls_config import LanguageServerConfig
 13 | from solidlsp.ls_logger import LanguageServerLogger
 14 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 15 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 16 | from solidlsp.settings import SolidLSPSettings
 17 | 
 18 | 
 19 | class SourceKitLSP(SolidLanguageServer):
 20 |     """
 21 |     Provides Swift specific instantiation of the LanguageServer class using sourcekit-lsp.
 22 |     """
 23 | 
 24 |     @override
 25 |     def is_ignored_dirname(self, dirname: str) -> bool:
 26 |         # For Swift projects, we should ignore:
 27 |         # - .build: Swift Package Manager build artifacts
 28 |         # - .swiftpm: Swift Package Manager metadata
 29 |         # - node_modules: if the project has JavaScript components
 30 |         # - dist/build: common output directories
 31 |         return super().is_ignored_dirname(dirname) or dirname in [".build", ".swiftpm", "node_modules", "dist", "build"]
 32 | 
 33 |     @staticmethod
 34 |     def _get_sourcekit_lsp_version() -> str:
 35 |         """Get the installed sourcekit-lsp version or raise error if sourcekit was not found."""
 36 |         try:
 37 |             result = subprocess.run(["sourcekit-lsp", "-h"], capture_output=True, text=True, check=False)
 38 |             if result.returncode == 0:
 39 |                 return result.stdout.strip()
 40 |             else:
 41 |                 raise Exception(f"`sourcekit-lsp -h` resulted in: {result}")
 42 |         except Exception as e:
 43 |             raise RuntimeError(
 44 |                 "Could not find sourcekit-lsp, please install it as described in https://github.com/apple/sourcekit-lsp#installation"
 45 |                 "And make sure it is available on your PATH."
 46 |             ) from e
 47 | 
 48 |     def __init__(
 49 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 50 |     ):
 51 |         sourcekit_version = self._get_sourcekit_lsp_version()
 52 |         logger.log(f"Starting sourcekit lsp with version: {sourcekit_version}", logging.INFO)
 53 | 
 54 |         super().__init__(
 55 |             config,
 56 |             logger,
 57 |             repository_root_path,
 58 |             ProcessLaunchInfo(cmd="sourcekit-lsp", cwd=repository_root_path),
 59 |             "swift",
 60 |             solidlsp_settings,
 61 |         )
 62 |         self.server_ready = threading.Event()
 63 |         self.request_id = 0
 64 |         self._did_sleep_before_requesting_references = False
 65 |         self._initialization_timestamp = None
 66 | 
 67 |     @staticmethod
 68 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
 69 |         """
 70 |         Returns the initialize params for the Swift Language Server.
 71 |         """
 72 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
 73 | 
 74 |         initialize_params = {
 75 |             "capabilities": {
 76 |                 "general": {
 77 |                     "markdown": {"parser": "marked", "version": "1.1.0"},
 78 |                     "positionEncodings": ["utf-16"],
 79 |                     "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"},
 80 |                     "staleRequestSupport": {
 81 |                         "cancel": True,
 82 |                         "retryOnContentModified": [
 83 |                             "textDocument/semanticTokens/full",
 84 |                             "textDocument/semanticTokens/range",
 85 |                             "textDocument/semanticTokens/full/delta",
 86 |                         ],
 87 |                     },
 88 |                 },
 89 |                 "notebookDocument": {"synchronization": {"dynamicRegistration": True, "executionSummarySupport": True}},
 90 |                 "textDocument": {
 91 |                     "callHierarchy": {"dynamicRegistration": True},
 92 |                     "codeAction": {
 93 |                         "codeActionLiteralSupport": {
 94 |                             "codeActionKind": {
 95 |                                 "valueSet": [
 96 |                                     "",
 97 |                                     "quickfix",
 98 |                                     "refactor",
 99 |                                     "refactor.extract",
100 |                                     "refactor.inline",
101 |                                     "refactor.rewrite",
102 |                                     "source",
103 |                                     "source.organizeImports",
104 |                                 ]
105 |                             }
106 |                         },
107 |                         "dataSupport": True,
108 |                         "disabledSupport": True,
109 |                         "dynamicRegistration": True,
110 |                         "honorsChangeAnnotations": True,
111 |                         "isPreferredSupport": True,
112 |                         "resolveSupport": {"properties": ["edit"]},
113 |                     },
114 |                     "codeLens": {"dynamicRegistration": True},
115 |                     "colorProvider": {"dynamicRegistration": True},
116 |                     "completion": {
117 |                         "completionItem": {
118 |                             "commitCharactersSupport": True,
119 |                             "deprecatedSupport": True,
120 |                             "documentationFormat": ["markdown", "plaintext"],
121 |                             "insertReplaceSupport": True,
122 |                             "insertTextModeSupport": {"valueSet": [1, 2]},
123 |                             "labelDetailsSupport": True,
124 |                             "preselectSupport": True,
125 |                             "resolveSupport": {"properties": ["documentation", "detail", "additionalTextEdits"]},
126 |                             "snippetSupport": True,
127 |                             "tagSupport": {"valueSet": [1]},
128 |                         },
129 |                         "completionItemKind": {
130 |                             "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
131 |                         },
132 |                         "completionList": {"itemDefaults": ["commitCharacters", "editRange", "insertTextFormat", "insertTextMode", "data"]},
133 |                         "contextSupport": True,
134 |                         "dynamicRegistration": True,
135 |                         "insertTextMode": 2,
136 |                     },
137 |                     "declaration": {"dynamicRegistration": True, "linkSupport": True},
138 |                     "definition": {"dynamicRegistration": True, "linkSupport": True},
139 |                     "diagnostic": {"dynamicRegistration": True, "relatedDocumentSupport": False},
140 |                     "documentHighlight": {"dynamicRegistration": True},
141 |                     "documentLink": {"dynamicRegistration": True, "tooltipSupport": True},
142 |                     "documentSymbol": {
143 |                         "dynamicRegistration": True,
144 |                         "hierarchicalDocumentSymbolSupport": True,
145 |                         "labelSupport": True,
146 |                         "symbolKind": {
147 |                             "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
148 |                         },
149 |                         "tagSupport": {"valueSet": [1]},
150 |                     },
151 |                     "foldingRange": {
152 |                         "dynamicRegistration": True,
153 |                         "foldingRange": {"collapsedText": False},
154 |                         "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]},
155 |                         "lineFoldingOnly": True,
156 |                         "rangeLimit": 5000,
157 |                     },
158 |                     "formatting": {"dynamicRegistration": True},
159 |                     "hover": {"contentFormat": ["markdown", "plaintext"], "dynamicRegistration": True},
160 |                     "implementation": {"dynamicRegistration": True, "linkSupport": True},
161 |                     "inlayHint": {
162 |                         "dynamicRegistration": True,
163 |                         "resolveSupport": {"properties": ["tooltip", "textEdits", "label.tooltip", "label.location", "label.command"]},
164 |                     },
165 |                     "inlineValue": {"dynamicRegistration": True},
166 |                     "linkedEditingRange": {"dynamicRegistration": True},
167 |                     "onTypeFormatting": {"dynamicRegistration": True},
168 |                     "publishDiagnostics": {
169 |                         "codeDescriptionSupport": True,
170 |                         "dataSupport": True,
171 |                         "relatedInformation": True,
172 |                         "tagSupport": {"valueSet": [1, 2]},
173 |                         "versionSupport": False,
174 |                     },
175 |                     "rangeFormatting": {"dynamicRegistration": True, "rangesSupport": True},
176 |                     "references": {"dynamicRegistration": True},
177 |                     "rename": {
178 |                         "dynamicRegistration": True,
179 |                         "honorsChangeAnnotations": True,
180 |                         "prepareSupport": True,
181 |                         "prepareSupportDefaultBehavior": 1,
182 |                     },
183 |                     "selectionRange": {"dynamicRegistration": True},
184 |                     "semanticTokens": {
185 |                         "augmentsSyntaxTokens": True,
186 |                         "dynamicRegistration": True,
187 |                         "formats": ["relative"],
188 |                         "multilineTokenSupport": False,
189 |                         "overlappingTokenSupport": False,
190 |                         "requests": {"full": {"delta": True}, "range": True},
191 |                         "serverCancelSupport": True,
192 |                         "tokenModifiers": [
193 |                             "declaration",
194 |                             "definition",
195 |                             "readonly",
196 |                             "static",
197 |                             "deprecated",
198 |                             "abstract",
199 |                             "async",
200 |                             "modification",
201 |                             "documentation",
202 |                             "defaultLibrary",
203 |                         ],
204 |                         "tokenTypes": [
205 |                             "namespace",
206 |                             "type",
207 |                             "class",
208 |                             "enum",
209 |                             "interface",
210 |                             "struct",
211 |                             "typeParameter",
212 |                             "parameter",
213 |                             "variable",
214 |                             "property",
215 |                             "enumMember",
216 |                             "event",
217 |                             "function",
218 |                             "method",
219 |                             "macro",
220 |                             "keyword",
221 |                             "modifier",
222 |                             "comment",
223 |                             "string",
224 |                             "number",
225 |                             "regexp",
226 |                             "operator",
227 |                             "decorator",
228 |                         ],
229 |                     },
230 |                     "signatureHelp": {
231 |                         "contextSupport": True,
232 |                         "dynamicRegistration": True,
233 |                         "signatureInformation": {
234 |                             "activeParameterSupport": True,
235 |                             "documentationFormat": ["markdown", "plaintext"],
236 |                             "parameterInformation": {"labelOffsetSupport": True},
237 |                         },
238 |                     },
239 |                     "synchronization": {"didSave": True, "dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True},
240 |                     "typeDefinition": {"dynamicRegistration": True, "linkSupport": True},
241 |                     "typeHierarchy": {"dynamicRegistration": True},
242 |                 },
243 |                 "window": {
244 |                     "showDocument": {"support": True},
245 |                     "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}},
246 |                     "workDoneProgress": True,
247 |                 },
248 |                 "workspace": {
249 |                     "applyEdit": True,
250 |                     "codeLens": {"refreshSupport": True},
251 |                     "configuration": True,
252 |                     "diagnostics": {"refreshSupport": True},
253 |                     "didChangeConfiguration": {"dynamicRegistration": True},
254 |                     "didChangeWatchedFiles": {"dynamicRegistration": True, "relativePatternSupport": True},
255 |                     "executeCommand": {"dynamicRegistration": True},
256 |                     "fileOperations": {
257 |                         "didCreate": True,
258 |                         "didDelete": True,
259 |                         "didRename": True,
260 |                         "dynamicRegistration": True,
261 |                         "willCreate": True,
262 |                         "willDelete": True,
263 |                         "willRename": True,
264 |                     },
265 |                     "foldingRange": {"refreshSupport": True},
266 |                     "inlayHint": {"refreshSupport": True},
267 |                     "inlineValue": {"refreshSupport": True},
268 |                     "semanticTokens": {"refreshSupport": False},
269 |                     "symbol": {
270 |                         "dynamicRegistration": True,
271 |                         "resolveSupport": {"properties": ["location.range"]},
272 |                         "symbolKind": {
273 |                             "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]
274 |                         },
275 |                         "tagSupport": {"valueSet": [1]},
276 |                     },
277 |                     "workspaceEdit": {
278 |                         "changeAnnotationSupport": {"groupsOnLabel": True},
279 |                         "documentChanges": True,
280 |                         "failureHandling": "textOnlyTransactional",
281 |                         "normalizesLineEndings": True,
282 |                         "resourceOperations": ["create", "rename", "delete"],
283 |                     },
284 |                     "workspaceFolders": True,
285 |                 },
286 |             },
287 |             "clientInfo": {"name": "Visual Studio Code", "version": "1.102.2"},
288 |             "initializationOptions": {
289 |                 "backgroundIndexing": True,
290 |                 "backgroundPreparationMode": "enabled",
291 |                 "textDocument/codeLens": {"supportedCommands": {"swift.debug": "swift.debug", "swift.run": "swift.run"}},
292 |                 "window/didChangeActiveDocument": True,
293 |                 "workspace/getReferenceDocument": True,
294 |                 "workspace/peekDocuments": True,
295 |             },
296 |             "locale": "en",
297 |             "processId": os.getpid(),
298 |             "rootPath": repository_absolute_path,
299 |             "rootUri": root_uri,
300 |             "workspaceFolders": [
301 |                 {
302 |                     "uri": root_uri,
303 |                     "name": os.path.basename(repository_absolute_path),
304 |                 }
305 |             ],
306 |         }
307 | 
308 |         return initialize_params
309 | 
310 |     def _start_server(self):
311 |         """Start sourcekit-lsp server process"""
312 | 
313 |         def register_capability_handler(_params):
314 |             return
315 | 
316 |         def window_log_message(msg):
317 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
318 | 
319 |         def do_nothing(_params):
320 |             return
321 | 
322 |         self.server.on_request("client/registerCapability", register_capability_handler)
323 |         self.server.on_notification("window/logMessage", window_log_message)
324 |         self.server.on_notification("$/progress", do_nothing)
325 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
326 | 
327 |         self.logger.log("Starting sourcekit-lsp server process", logging.INFO)
328 |         self.server.start()
329 |         initialize_params = self._get_initialize_params(self.repository_root_path)
330 | 
331 |         self.logger.log(
332 |             "Sending initialize request from LSP client to LSP server and awaiting response",
333 |             logging.INFO,
334 |         )
335 |         init_response = self.server.send.initialize(initialize_params)
336 | 
337 |         capabilities = init_response["capabilities"]
338 |         self.logger.log(f"SourceKit LSP capabilities: {list(capabilities.keys())}", logging.INFO)
339 | 
340 |         assert "textDocumentSync" in capabilities, "textDocumentSync capability missing"
341 |         assert "definitionProvider" in capabilities, "definitionProvider capability missing"
342 | 
343 |         self.server.notify.initialized({})
344 |         self.completions_available.set()
345 | 
346 |         self.server_ready.set()
347 |         self.server_ready.wait()
348 | 
349 |         # Mark initialization timestamp for smarter delay calculation
350 |         self._initialization_timestamp = time.time()
351 | 
352 |     @override
353 |     def request_references(self, relative_file_path: str, line: int, column: int) -> list[ls_types.Location]:
354 |         # SourceKit LSP needs initialization + indexing time after startup
355 |         # before it can provide accurate reference information. This sleep
356 |         # prevents race conditions where references might not be available yet.
357 |         # CI environments need extra time for project indexing and cross-file analysis
358 |         if not self._did_sleep_before_requesting_references:
359 |             # Calculate minimum delay based on how much time has passed since initialization
360 |             if self._initialization_timestamp:
361 |                 elapsed = time.time() - self._initialization_timestamp
362 |                 # Increased CI delay for project indexing: 15s CI, 5s local
363 |                 base_delay = 15 if os.getenv("CI") else 5
364 |                 remaining_delay = max(2, base_delay - elapsed)
365 |             else:
366 |                 # Fallback if initialization timestamp is missing
367 |                 remaining_delay = 15 if os.getenv("CI") else 5
368 | 
369 |             self.logger.log(
370 |                 f"Sleeping {remaining_delay:.1f}s before requesting references for the first time (CI needs extra indexing time)",
371 |                 logging.INFO,
372 |             )
373 |             time.sleep(remaining_delay)
374 |             self._did_sleep_before_requesting_references = True
375 | 
376 |         # Get references with retry logic for CI stability
377 |         references = super().request_references(relative_file_path, line, column)
378 | 
379 |         # In CI, if no references found, retry once after additional delay
380 |         if os.getenv("CI") and not references:
381 |             self.logger.log("No references found in CI - retrying after additional 5s delay", logging.INFO)
382 |             time.sleep(5)
383 |             references = super().request_references(relative_file_path, line, column)
384 | 
385 |         return references
386 | 
```

--------------------------------------------------------------------------------
/src/serena/tools/file_tools.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | File and file system-related tools, specifically for
  3 |   * listing directory contents
  4 |   * reading files
  5 |   * creating files
  6 |   * editing at the file level
  7 | """
  8 | 
  9 | import json
 10 | import os
 11 | import re
 12 | from collections import defaultdict
 13 | from fnmatch import fnmatch
 14 | from pathlib import Path
 15 | 
 16 | from serena.text_utils import search_files
 17 | from serena.tools import SUCCESS_RESULT, EditedFileContext, Tool, ToolMarkerCanEdit, ToolMarkerOptional
 18 | from serena.util.file_system import scan_directory
 19 | 
 20 | 
 21 | class ReadFileTool(Tool):
 22 |     """
 23 |     Reads a file within the project directory.
 24 |     """
 25 | 
 26 |     def apply(self, relative_path: str, start_line: int = 0, end_line: int | None = None, max_answer_chars: int = -1) -> str:
 27 |         """
 28 |         Reads the given file or a chunk of it. Generally, symbolic operations
 29 |         like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for.
 30 | 
 31 |         :param relative_path: the relative path to the file to read
 32 |         :param start_line: the 0-based index of the first line to be retrieved.
 33 |         :param end_line: the 0-based index of the last line to be retrieved (inclusive). If None, read until the end of the file.
 34 |         :param max_answer_chars: if the file (chunk) is longer than this number of characters,
 35 |             no content will be returned. Don't adjust unless there is really no other way to get the content
 36 |             required for the task.
 37 |         :return: the full text of the file at the given relative path
 38 |         """
 39 |         self.project.validate_relative_path(relative_path, require_not_ignored=True)
 40 | 
 41 |         result = self.project.read_file(relative_path)
 42 |         result_lines = result.splitlines()
 43 |         if end_line is None:
 44 |             result_lines = result_lines[start_line:]
 45 |         else:
 46 |             result_lines = result_lines[start_line : end_line + 1]
 47 |         result = "\n".join(result_lines)
 48 | 
 49 |         return self._limit_length(result, max_answer_chars)
 50 | 
 51 | 
 52 | class CreateTextFileTool(Tool, ToolMarkerCanEdit):
 53 |     """
 54 |     Creates/overwrites a file in the project directory.
 55 |     """
 56 | 
 57 |     def apply(self, relative_path: str, content: str) -> str:
 58 |         """
 59 |         Write a new file or overwrite an existing file.
 60 | 
 61 |         :param relative_path: the relative path to the file to create
 62 |         :param content: the (utf-8-encoded) content to write to the file
 63 |         :return: a message indicating success or failure
 64 |         """
 65 |         project_root = self.get_project_root()
 66 |         abs_path = (Path(project_root) / relative_path).resolve()
 67 |         will_overwrite_existing = abs_path.exists()
 68 | 
 69 |         if will_overwrite_existing:
 70 |             self.project.validate_relative_path(relative_path, require_not_ignored=True)
 71 |         else:
 72 |             assert abs_path.is_relative_to(
 73 |                 self.get_project_root()
 74 |             ), f"Cannot create file outside of the project directory, got {relative_path=}"
 75 | 
 76 |         abs_path.parent.mkdir(parents=True, exist_ok=True)
 77 |         abs_path.write_text(content, encoding="utf-8")
 78 |         answer = f"File created: {relative_path}."
 79 |         if will_overwrite_existing:
 80 |             answer += " Overwrote existing file."
 81 |         return json.dumps(answer)
 82 | 
 83 | 
 84 | class ListDirTool(Tool):
 85 |     """
 86 |     Lists files and directories in the given directory (optionally with recursion).
 87 |     """
 88 | 
 89 |     def apply(self, relative_path: str, recursive: bool, skip_ignored_files: bool = False, max_answer_chars: int = -1) -> str:
 90 |         """
 91 |         Lists files and directories in the given directory (optionally with recursion).
 92 | 
 93 |         :param relative_path: the relative path to the directory to list; pass "." to scan the project root
 94 |         :param recursive: whether to scan subdirectories recursively
 95 |         :param skip_ignored_files: whether to skip files and directories that are ignored
 96 |         :param max_answer_chars: if the output is longer than this number of characters,
 97 |             no content will be returned. -1 means the default value from the config will be used.
 98 |             Don't adjust unless there is really no other way to get the content required for the task.
 99 |         :return: a JSON object with the names of directories and files within the given directory
100 |         """
101 |         # Check if the directory exists before validation
102 |         if not self.project.relative_path_exists(relative_path):
103 |             error_info = {
104 |                 "error": f"Directory not found: {relative_path}",
105 |                 "project_root": self.get_project_root(),
106 |                 "hint": "Check if the path is correct relative to the project root",
107 |             }
108 |             return json.dumps(error_info)
109 | 
110 |         self.project.validate_relative_path(relative_path, require_not_ignored=skip_ignored_files)
111 | 
112 |         dirs, files = scan_directory(
113 |             os.path.join(self.get_project_root(), relative_path),
114 |             relative_to=self.get_project_root(),
115 |             recursive=recursive,
116 |             is_ignored_dir=self.project.is_ignored_path if skip_ignored_files else None,
117 |             is_ignored_file=self.project.is_ignored_path if skip_ignored_files else None,
118 |         )
119 | 
120 |         result = json.dumps({"dirs": dirs, "files": files})
121 |         return self._limit_length(result, max_answer_chars)
122 | 
123 | 
124 | class FindFileTool(Tool):
125 |     """
126 |     Finds files in the given relative paths
127 |     """
128 | 
129 |     def apply(self, file_mask: str, relative_path: str) -> str:
130 |         """
131 |         Finds non-gitignored files matching the given file mask within the given relative path
132 | 
133 |         :param file_mask: the filename or file mask (using the wildcards * or ?) to search for
134 |         :param relative_path: the relative path to the directory to search in; pass "." to scan the project root
135 |         :return: a JSON object with the list of matching files
136 |         """
137 |         self.project.validate_relative_path(relative_path, require_not_ignored=True)
138 | 
139 |         dir_to_scan = os.path.join(self.get_project_root(), relative_path)
140 | 
141 |         # find the files by ignoring everything that doesn't match
142 |         def is_ignored_file(abs_path: str) -> bool:
143 |             if self.project.is_ignored_path(abs_path):
144 |                 return True
145 |             filename = os.path.basename(abs_path)
146 |             return not fnmatch(filename, file_mask)
147 | 
148 |         _dirs, files = scan_directory(
149 |             path=dir_to_scan,
150 |             recursive=True,
151 |             is_ignored_dir=self.project.is_ignored_path,
152 |             is_ignored_file=is_ignored_file,
153 |             relative_to=self.get_project_root(),
154 |         )
155 | 
156 |         result = json.dumps({"files": files})
157 |         return result
158 | 
159 | 
160 | class ReplaceRegexTool(Tool, ToolMarkerCanEdit):
161 |     """
162 |     Replaces content in a file by using regular expressions.
163 |     """
164 | 
165 |     def apply(
166 |         self,
167 |         relative_path: str,
168 |         regex: str,
169 |         repl: str,
170 |         allow_multiple_occurrences: bool = False,
171 |     ) -> str:
172 |         r"""
173 |         Replaces one or more occurrences of the given regular expression.
174 |         This is the preferred way to replace content in a file whenever the symbol-level
175 |         tools are not appropriate.
176 |         Even large sections of code can be replaced by providing a concise regular expression of
177 |         the form "beginning.*?end-of-text-to-be-replaced".
178 |         Always try to use wildcards to avoid specifying the exact content of the code to be replaced,
179 |         especially if it spans several lines.
180 | 
181 |         IMPORTANT: REMEMBER TO USE WILDCARDS WHEN APPROPRIATE! I WILL BE VERY UNHAPPY IF YOU WRITE UNNECESSARILY LONG REGEXES WITHOUT USING WILDCARDS!
182 | 
183 |         :param relative_path: the relative path to the file
184 |         :param regex: a Python-style regular expression, matches of which will be replaced.
185 |             Dot matches all characters, multi-line matching is enabled.
186 |         :param repl: the string to replace the matched content with, which may contain
187 |             backreferences like \1, \2, etc.
188 |             IMPORTANT: Make sure to escape special characters appropriately!
189 |                 Use "\n" to insert a newline, but use "\\n" to insert the string "\n" within a string literal.
190 |         :param allow_multiple_occurrences: if True, the regex may match multiple occurrences in the file
191 |             and all of them will be replaced.
192 |             If this is set to False and the regex matches multiple occurrences, an error will be returned
193 |             (and you may retry with a revised, more specific regex).
194 |         """
195 |         self.project.validate_relative_path(relative_path, require_not_ignored=True)
196 |         with EditedFileContext(relative_path, self.agent) as context:
197 |             original_content = context.get_original_content()
198 |             updated_content, n = re.subn(regex, repl, original_content, flags=re.DOTALL | re.MULTILINE)
199 |             if n == 0:
200 |                 return f"Error: No matches found for regex '{regex}' in file '{relative_path}'."
201 |             if not allow_multiple_occurrences and n > 1:
202 |                 return (
203 |                     f"Error: Regex '{regex}' matches {n} occurrences in file '{relative_path}'. "
204 |                     "Please revise the regex to be more specific or enable allow_multiple_occurrences if this is expected."
205 |                 )
206 |             context.set_updated_content(updated_content)
207 |         return SUCCESS_RESULT
208 | 
209 | 
210 | class DeleteLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
211 |     """
212 |     Deletes a range of lines within a file.
213 |     """
214 | 
215 |     def apply(
216 |         self,
217 |         relative_path: str,
218 |         start_line: int,
219 |         end_line: int,
220 |     ) -> str:
221 |         """
222 |         Deletes the given lines in the file.
223 |         Requires that the same range of lines was previously read using the `read_file` tool to verify correctness
224 |         of the operation.
225 | 
226 |         :param relative_path: the relative path to the file
227 |         :param start_line: the 0-based index of the first line to be deleted
228 |         :param end_line: the 0-based index of the last line to be deleted
229 |         """
230 |         code_editor = self.create_code_editor()
231 |         code_editor.delete_lines(relative_path, start_line, end_line)
232 |         return SUCCESS_RESULT
233 | 
234 | 
235 | class ReplaceLinesTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
236 |     """
237 |     Replaces a range of lines within a file with new content.
238 |     """
239 | 
240 |     def apply(
241 |         self,
242 |         relative_path: str,
243 |         start_line: int,
244 |         end_line: int,
245 |         content: str,
246 |     ) -> str:
247 |         """
248 |         Replaces the given range of lines in the given file.
249 |         Requires that the same range of lines was previously read using the `read_file` tool to verify correctness
250 |         of the operation.
251 | 
252 |         :param relative_path: the relative path to the file
253 |         :param start_line: the 0-based index of the first line to be deleted
254 |         :param end_line: the 0-based index of the last line to be deleted
255 |         :param content: the content to insert
256 |         """
257 |         if not content.endswith("\n"):
258 |             content += "\n"
259 |         result = self.agent.get_tool(DeleteLinesTool).apply(relative_path, start_line, end_line)
260 |         if result != SUCCESS_RESULT:
261 |             return result
262 |         self.agent.get_tool(InsertAtLineTool).apply(relative_path, start_line, content)
263 |         return SUCCESS_RESULT
264 | 
265 | 
266 | class InsertAtLineTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
267 |     """
268 |     Inserts content at a given line in a file.
269 |     """
270 | 
271 |     def apply(
272 |         self,
273 |         relative_path: str,
274 |         line: int,
275 |         content: str,
276 |     ) -> str:
277 |         """
278 |         Inserts the given content at the given line in the file, pushing existing content of the line down.
279 |         In general, symbolic insert operations like insert_after_symbol or insert_before_symbol should be preferred if you know which
280 |         symbol you are looking for.
281 |         However, this can also be useful for small targeted edits of the body of a longer symbol (without replacing the entire body).
282 | 
283 |         :param relative_path: the relative path to the file
284 |         :param line: the 0-based index of the line to insert content at
285 |         :param content: the content to be inserted
286 |         """
287 |         if not content.endswith("\n"):
288 |             content += "\n"
289 |         code_editor = self.create_code_editor()
290 |         code_editor.insert_at_line(relative_path, line, content)
291 |         return SUCCESS_RESULT
292 | 
293 | 
294 | class SearchForPatternTool(Tool):
295 |     """
296 |     Performs a search for a pattern in the project.
297 |     """
298 | 
299 |     def apply(
300 |         self,
301 |         substring_pattern: str,
302 |         context_lines_before: int = 0,
303 |         context_lines_after: int = 0,
304 |         paths_include_glob: str = "",
305 |         paths_exclude_glob: str = "",
306 |         relative_path: str = "",
307 |         restrict_search_to_code_files: bool = False,
308 |         max_answer_chars: int = -1,
309 |     ) -> str:
310 |         """
311 |         Offers a flexible search for arbitrary patterns in the codebase, including the
312 |         possibility to search in non-code files.
313 |         Generally, symbolic operations like find_symbol or find_referencing_symbols
314 |         should be preferred if you know which symbols you are looking for.
315 | 
316 |         Pattern Matching Logic:
317 |             For each match, the returned result will contain the full lines where the
318 |             substring pattern is found, as well as optionally some lines before and after it. The pattern will be compiled with
319 |             DOTALL, meaning that the dot will match all characters including newlines.
320 |             This also means that it never makes sense to have .* at the beginning or end of the pattern,
321 |             but it may make sense to have it in the middle for complex patterns.
322 |             If a pattern matches multiple lines, all those lines will be part of the match.
323 |             Be careful to not use greedy quantifiers unnecessarily, it is usually better to use non-greedy quantifiers like .*? to avoid
324 |             matching too much content.
325 | 
326 |         File Selection Logic:
327 |             The files in which the search is performed can be restricted very flexibly.
328 |             Using `restrict_search_to_code_files` is useful if you are only interested in code symbols (i.e., those
329 |             symbols that can be manipulated with symbolic tools like find_symbol).
330 |             You can also restrict the search to a specific file or directory,
331 |             and provide glob patterns to include or exclude certain files on top of that.
332 |             The globs are matched against relative file paths from the project root (not to the `relative_path` parameter that
333 |             is used to further restrict the search).
334 |             Smartly combining the various restrictions allows you to perform very targeted searches.
335 | 
336 | 
337 |         :param substring_pattern: Regular expression for a substring pattern to search for
338 |         :param context_lines_before: Number of lines of context to include before each match
339 |         :param context_lines_after: Number of lines of context to include after each match
340 |         :param paths_include_glob: optional glob pattern specifying files to include in the search.
341 |             Matches against relative file paths from the project root (e.g., "*.py", "src/**/*.ts").
342 |             Supports standard glob patterns (*, ?, [seq], **, etc.) and brace expansion {a,b,c}.
343 |             Only matches files, not directories. If left empty, all non-ignored files will be included.
344 |         :param paths_exclude_glob: optional glob pattern specifying files to exclude from the search.
345 |             Matches against relative file paths from the project root (e.g., "*test*", "**/*_generated.py").
346 |             Supports standard glob patterns (*, ?, [seq], **, etc.) and brace expansion {a,b,c}.
347 |             Takes precedence over paths_include_glob. Only matches files, not directories. If left empty, no files are excluded.
348 |         :param relative_path: only subpaths of this path (relative to the repo root) will be analyzed. If a path to a single
349 |             file is passed, only that will be searched. The path must exist, otherwise a `FileNotFoundError` is raised.
350 |         :param max_answer_chars: if the output is longer than this number of characters,
351 |             no content will be returned.
352 |             -1 means the default value from the config will be used.
353 |             Don't adjust unless there is really no other way to get the content
354 |             required for the task. Instead, if the output is too long, you should
355 |             make a stricter query.
356 |         :param restrict_search_to_code_files: whether to restrict the search to only those files where
357 |             analyzed code symbols can be found. Otherwise, will search all non-ignored files.
358 |             Set this to True if your search is only meant to discover code that can be manipulated with symbolic tools.
359 |             For example, for finding classes or methods from a name pattern.
360 |             Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files,
361 |             which is why it is the default.
362 |         :return: A mapping of file paths to lists of matched consecutive lines.
363 |         """
364 |         abs_path = os.path.join(self.get_project_root(), relative_path)
365 |         if not os.path.exists(abs_path):
366 |             raise FileNotFoundError(f"Relative path {relative_path} does not exist.")
367 | 
368 |         if restrict_search_to_code_files:
369 |             matches = self.project.search_source_files_for_pattern(
370 |                 pattern=substring_pattern,
371 |                 relative_path=relative_path,
372 |                 context_lines_before=context_lines_before,
373 |                 context_lines_after=context_lines_after,
374 |                 paths_include_glob=paths_include_glob.strip(),
375 |                 paths_exclude_glob=paths_exclude_glob.strip(),
376 |             )
377 |         else:
378 |             if os.path.isfile(abs_path):
379 |                 rel_paths_to_search = [relative_path]
380 |             else:
381 |                 _dirs, rel_paths_to_search = scan_directory(
382 |                     path=abs_path,
383 |                     recursive=True,
384 |                     is_ignored_dir=self.project.is_ignored_path,
385 |                     is_ignored_file=self.project.is_ignored_path,
386 |                     relative_to=self.get_project_root(),
387 |                 )
388 |             # TODO (maybe): not super efficient to walk through the files again and filter if glob patterns are provided
389 |             #   but it probably never matters and this version required no further refactoring
390 |             matches = search_files(
391 |                 rel_paths_to_search,
392 |                 substring_pattern,
393 |                 root_path=self.get_project_root(),
394 |                 paths_include_glob=paths_include_glob,
395 |                 paths_exclude_glob=paths_exclude_glob,
396 |             )
397 |         # group matches by file
398 |         file_to_matches: dict[str, list[str]] = defaultdict(list)
399 |         for match in matches:
400 |             assert match.source_file_path is not None
401 |             file_to_matches[match.source_file_path].append(match.to_display_string())
402 |         result = json.dumps(file_to_matches)
403 |         return self._limit_length(result, max_answer_chars)
404 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/ls_request.py:
--------------------------------------------------------------------------------

```python
  1 | from typing import TYPE_CHECKING, Any, Union
  2 | 
  3 | from solidlsp.lsp_protocol_handler import lsp_types
  4 | 
  5 | if TYPE_CHECKING:
  6 |     from .ls_handler import SolidLanguageServerHandler
  7 | 
  8 | 
  9 | class LanguageServerRequest:
 10 |     def __init__(self, handler: "SolidLanguageServerHandler"):
 11 |         self.handler = handler
 12 | 
 13 |     def _send_request(self, method: str, params: Any | None = None) -> Any:
 14 |         return self.handler.send_request(method, params)
 15 | 
 16 |     def implementation(self, params: lsp_types.ImplementationParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
 17 |         """A request to resolve the implementation locations of a symbol at a given text
 18 |         document position. The request's parameter is of type [TextDocumentPositionParams]
 19 |         (#TextDocumentPositionParams) the response is of type {@link Definition} or a
 20 |         Thenable that resolves to such.
 21 |         """
 22 |         return self._send_request("textDocument/implementation", params)
 23 | 
 24 |     def type_definition(
 25 |         self, params: lsp_types.TypeDefinitionParams
 26 |     ) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
 27 |         """A request to resolve the type definition locations of a symbol at a given text
 28 |         document position. The request's parameter is of type [TextDocumentPositionParams]
 29 |         (#TextDocumentPositionParams) the response is of type {@link Definition} or a
 30 |         Thenable that resolves to such.
 31 |         """
 32 |         return self._send_request("textDocument/typeDefinition", params)
 33 | 
 34 |     def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]:
 35 |         """A request to list all color symbols found in a given text document. The request's
 36 |         parameter is of type {@link DocumentColorParams} the
 37 |         response is of type {@link ColorInformation ColorInformation[]} or a Thenable
 38 |         that resolves to such.
 39 |         """
 40 |         return self._send_request("textDocument/documentColor", params)
 41 | 
 42 |     def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]:
 43 |         """A request to list all presentation for a color. The request's
 44 |         parameter is of type {@link ColorPresentationParams} the
 45 |         response is of type {@link ColorInformation ColorInformation[]} or a Thenable
 46 |         that resolves to such.
 47 |         """
 48 |         return self._send_request("textDocument/colorPresentation", params)
 49 | 
 50 |     def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None:
 51 |         """A request to provide folding ranges in a document. The request's
 52 |         parameter is of type {@link FoldingRangeParams}, the
 53 |         response is of type {@link FoldingRangeList} or a Thenable
 54 |         that resolves to such.
 55 |         """
 56 |         return self._send_request("textDocument/foldingRange", params)
 57 | 
 58 |     def declaration(self, params: lsp_types.DeclarationParams) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]:
 59 |         """A request to resolve the type definition locations of a symbol at a given text
 60 |         document position. The request's parameter is of type [TextDocumentPositionParams]
 61 |         (#TextDocumentPositionParams) the response is of type {@link Declaration}
 62 |         or a typed array of {@link DeclarationLink} or a Thenable that resolves
 63 |         to such.
 64 |         """
 65 |         return self._send_request("textDocument/declaration", params)
 66 | 
 67 |     def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None:
 68 |         """A request to provide selection ranges in a document. The request's
 69 |         parameter is of type {@link SelectionRangeParams}, the
 70 |         response is of type {@link SelectionRange SelectionRange[]} or a Thenable
 71 |         that resolves to such.
 72 |         """
 73 |         return self._send_request("textDocument/selectionRange", params)
 74 | 
 75 |     def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None:
 76 |         """A request to result a `CallHierarchyItem` in a document at a given position.
 77 |         Can be used as an input to an incoming or outgoing call hierarchy.
 78 | 
 79 |         @since 3.16.0
 80 |         """
 81 |         return self._send_request("textDocument/prepareCallHierarchy", params)
 82 | 
 83 |     def incoming_calls(self, params: lsp_types.CallHierarchyIncomingCallsParams) -> list["lsp_types.CallHierarchyIncomingCall"] | None:
 84 |         """A request to resolve the incoming calls for a given `CallHierarchyItem`.
 85 | 
 86 |         @since 3.16.0
 87 |         """
 88 |         return self._send_request("callHierarchy/incomingCalls", params)
 89 | 
 90 |     def outgoing_calls(self, params: lsp_types.CallHierarchyOutgoingCallsParams) -> list["lsp_types.CallHierarchyOutgoingCall"] | None:
 91 |         """A request to resolve the outgoing calls for a given `CallHierarchyItem`.
 92 | 
 93 |         @since 3.16.0
 94 |         """
 95 |         return self._send_request("callHierarchy/outgoingCalls", params)
 96 | 
 97 |     def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]:
 98 |         """@since 3.16.0"""
 99 |         return self._send_request("textDocument/semanticTokens/full", params)
100 | 
101 |     def semantic_tokens_delta(
102 |         self, params: lsp_types.SemanticTokensDeltaParams
103 |     ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]:
104 |         """@since 3.16.0"""
105 |         return self._send_request("textDocument/semanticTokens/full/delta", params)
106 | 
107 |     def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]:
108 |         """@since 3.16.0"""
109 |         return self._send_request("textDocument/semanticTokens/range", params)
110 | 
111 |     def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]:
112 |         """A request to provide ranges that can be edited together.
113 | 
114 |         @since 3.16.0
115 |         """
116 |         return self._send_request("textDocument/linkedEditingRange", params)
117 | 
118 |     def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
119 |         """The will create files request is sent from the client to the server before files are actually
120 |         created as long as the creation is triggered from within the client.
121 | 
122 |         @since 3.16.0
123 |         """
124 |         return self._send_request("workspace/willCreateFiles", params)
125 | 
126 |     def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
127 |         """The will rename files request is sent from the client to the server before files are actually
128 |         renamed as long as the rename is triggered from within the client.
129 | 
130 |         @since 3.16.0
131 |         """
132 |         return self._send_request("workspace/willRenameFiles", params)
133 | 
134 |     def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]:
135 |         """The did delete files notification is sent from the client to the server when
136 |         files were deleted from within the client.
137 | 
138 |         @since 3.16.0
139 |         """
140 |         return self._send_request("workspace/willDeleteFiles", params)
141 | 
142 |     def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None:
143 |         """A request to get the moniker of a symbol at a given text document position.
144 |         The request parameter is of type {@link TextDocumentPositionParams}.
145 |         The response is of type {@link Moniker Moniker[]} or `null`.
146 |         """
147 |         return self._send_request("textDocument/moniker", params)
148 | 
149 |     def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None:
150 |         """A request to result a `TypeHierarchyItem` in a document at a given position.
151 |         Can be used as an input to a subtypes or supertypes type hierarchy.
152 | 
153 |         @since 3.17.0
154 |         """
155 |         return self._send_request("textDocument/prepareTypeHierarchy", params)
156 | 
157 |     def type_hierarchy_supertypes(self, params: lsp_types.TypeHierarchySupertypesParams) -> list["lsp_types.TypeHierarchyItem"] | None:
158 |         """A request to resolve the supertypes for a given `TypeHierarchyItem`.
159 | 
160 |         @since 3.17.0
161 |         """
162 |         return self._send_request("typeHierarchy/supertypes", params)
163 | 
164 |     def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None:
165 |         """A request to resolve the subtypes for a given `TypeHierarchyItem`.
166 | 
167 |         @since 3.17.0
168 |         """
169 |         return self._send_request("typeHierarchy/subtypes", params)
170 | 
171 |     def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None:
172 |         """A request to provide inline values in a document. The request's parameter is of
173 |         type {@link InlineValueParams}, the response is of type
174 |         {@link InlineValue InlineValue[]} or a Thenable that resolves to such.
175 | 
176 |         @since 3.17.0
177 |         """
178 |         return self._send_request("textDocument/inlineValue", params)
179 | 
180 |     def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None:
181 |         """A request to provide inlay hints in a document. The request's parameter is of
182 |         type {@link InlayHintsParams}, the response is of type
183 |         {@link InlayHint InlayHint[]} or a Thenable that resolves to such.
184 | 
185 |         @since 3.17.0
186 |         """
187 |         return self._send_request("textDocument/inlayHint", params)
188 | 
189 |     def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint":
190 |         """A request to resolve additional properties for an inlay hint.
191 |         The request's parameter is of type {@link InlayHint}, the response is
192 |         of type {@link InlayHint} or a Thenable that resolves to such.
193 | 
194 |         @since 3.17.0
195 |         """
196 |         return self._send_request("inlayHint/resolve", params)
197 | 
198 |     def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport":
199 |         """The document diagnostic request definition.
200 | 
201 |         @since 3.17.0
202 |         """
203 |         return self._send_request("textDocument/diagnostic", params)
204 | 
205 |     def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport":
206 |         """The workspace diagnostic request definition.
207 | 
208 |         @since 3.17.0
209 |         """
210 |         return self._send_request("workspace/diagnostic", params)
211 | 
212 |     def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult":
213 |         """The initialize request is sent from the client to the server.
214 |         It is sent once as the request after starting up the server.
215 |         The requests parameter is of type {@link InitializeParams}
216 |         the response if of type {@link InitializeResult} of a Thenable that
217 |         resolves to such.
218 |         """
219 |         return self._send_request("initialize", params)
220 | 
221 |     def shutdown(self) -> None:
222 |         """A shutdown request is sent from the client to the server.
223 |         It is sent once when the client decides to shutdown the
224 |         server. The only notification that is sent after a shutdown request
225 |         is the exit event.
226 |         """
227 |         return self._send_request("shutdown")
228 | 
229 |     def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None:
230 |         """A document will save request is sent from the client to the server before
231 |         the document is actually saved. The request can return an array of TextEdits
232 |         which will be applied to the text document before it is saved. Please note that
233 |         clients might drop results if computing the text edits took too long or if a
234 |         server constantly fails on this request. This is done to keep the save fast and
235 |         reliable.
236 |         """
237 |         return self._send_request("textDocument/willSaveWaitUntil", params)
238 | 
239 |     def completion(self, params: lsp_types.CompletionParams) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]:
240 |         """Request to request completion at a given text document position. The request's
241 |         parameter is of type {@link TextDocumentPosition} the response
242 |         is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList}
243 |         or a Thenable that resolves to such.
244 | 
245 |         The request can delay the computation of the {@link CompletionItem.detail `detail`}
246 |         and {@link CompletionItem.documentation `documentation`} properties to the `completionItem/resolve`
247 |         request. However, properties that are needed for the initial sorting and filtering, like `sortText`,
248 |         `filterText`, `insertText`, and `textEdit`, must not be changed during resolve.
249 |         """
250 |         return self._send_request("textDocument/completion", params)
251 | 
252 |     def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem":
253 |         """Request to resolve additional information for a given completion item.The request's
254 |         parameter is of type {@link CompletionItem} the response
255 |         is of type {@link CompletionItem} or a Thenable that resolves to such.
256 |         """
257 |         return self._send_request("completionItem/resolve", params)
258 | 
259 |     def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]:
260 |         """Request to request hover information at a given text document position. The request's
261 |         parameter is of type {@link TextDocumentPosition} the response is of
262 |         type {@link Hover} or a Thenable that resolves to such.
263 |         """
264 |         return self._send_request("textDocument/hover", params)
265 | 
266 |     def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]:
267 |         return self._send_request("textDocument/signatureHelp", params)
268 | 
269 |     def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]:
270 |         """A request to resolve the definition location of a symbol at a given text
271 |         document position. The request's parameter is of type [TextDocumentPosition]
272 |         (#TextDocumentPosition) the response is of either type {@link Definition}
273 |         or a typed array of {@link DefinitionLink} or a Thenable that resolves
274 |         to such.
275 |         """
276 |         return self._send_request("textDocument/definition", params)
277 | 
278 |     def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None:
279 |         """A request to resolve project-wide references for the symbol denoted
280 |         by the given text document position. The request's parameter is of
281 |         type {@link ReferenceParams} the response is of type
282 |         {@link Location Location[]} or a Thenable that resolves to such.
283 |         """
284 |         return self._send_request("textDocument/references", params)
285 | 
286 |     def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None:
287 |         """Request to resolve a {@link DocumentHighlight} for a given
288 |         text document position. The request's parameter is of type [TextDocumentPosition]
289 |         (#TextDocumentPosition) the request response is of type [DocumentHighlight[]]
290 |         (#DocumentHighlight) or a Thenable that resolves to such.
291 |         """
292 |         return self._send_request("textDocument/documentHighlight", params)
293 | 
294 |     def document_symbol(
295 |         self, params: lsp_types.DocumentSymbolParams
296 |     ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.DocumentSymbol"] | None:
297 |         """A request to list all symbols found in a given text document. The request's
298 |         parameter is of type {@link TextDocumentIdentifier} the
299 |         response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable
300 |         that resolves to such.
301 |         """
302 |         return self._send_request("textDocument/documentSymbol", params)
303 | 
304 |     def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None:
305 |         """A request to provide commands for the given text document and range."""
306 |         return self._send_request("textDocument/codeAction", params)
307 | 
308 |     def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction":
309 |         """Request to resolve additional information for a given code action.The request's
310 |         parameter is of type {@link CodeAction} the response
311 |         is of type {@link CodeAction} or a Thenable that resolves to such.
312 |         """
313 |         return self._send_request("codeAction/resolve", params)
314 | 
315 |     def workspace_symbol(
316 |         self, params: lsp_types.WorkspaceSymbolParams
317 |     ) -> list["lsp_types.SymbolInformation"] | list["lsp_types.WorkspaceSymbol"] | None:
318 |         """A request to list project-wide symbols matching the query string given
319 |         by the {@link WorkspaceSymbolParams}. The response is
320 |         of type {@link SymbolInformation SymbolInformation[]} or a Thenable that
321 |         resolves to such.
322 | 
323 |         @since 3.17.0 - support for WorkspaceSymbol in the returned data. Clients
324 |          need to advertise support for WorkspaceSymbols via the client capability
325 |          `workspace.symbol.resolveSupport`.
326 |         """
327 |         return self._send_request("workspace/symbol", params)
328 | 
329 |     def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol":
330 |         """A request to resolve the range inside the workspace
331 |         symbol's location.
332 | 
333 |         @since 3.17.0
334 |         """
335 |         return self._send_request("workspaceSymbol/resolve", params)
336 | 
337 |     def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None:
338 |         """A request to provide code lens for the given text document."""
339 |         return self._send_request("textDocument/codeLens", params)
340 | 
341 |     def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens":
342 |         """A request to resolve a command for a given code lens."""
343 |         return self._send_request("codeLens/resolve", params)
344 | 
345 |     def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None:
346 |         """A request to provide document links"""
347 |         return self._send_request("textDocument/documentLink", params)
348 | 
349 |     def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink":
350 |         """Request to resolve additional information for a given document link. The request's
351 |         parameter is of type {@link DocumentLink} the response
352 |         is of type {@link DocumentLink} or a Thenable that resolves to such.
353 |         """
354 |         return self._send_request("documentLink/resolve", params)
355 | 
356 |     def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None:
357 |         """A request to to format a whole document."""
358 |         return self._send_request("textDocument/formatting", params)
359 | 
360 |     def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None:
361 |         """A request to to format a range in a document."""
362 |         return self._send_request("textDocument/rangeFormatting", params)
363 | 
364 |     def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None:
365 |         """A request to format a document on type."""
366 |         return self._send_request("textDocument/onTypeFormatting", params)
367 | 
368 |     def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]:
369 |         """A request to rename a symbol."""
370 |         return self._send_request("textDocument/rename", params)
371 | 
372 |     def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]:
373 |         """A request to test and perform the setup necessary for a rename.
374 | 
375 |         @since 3.16 - support for default behavior
376 |         """
377 |         return self._send_request("textDocument/prepareRename", params)
378 | 
379 |     def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]:
380 |         """A request send from the client to the server to execute a command. The request might return
381 |         a workspace edit which the client will apply to the workspace.
382 |         """
383 |         return self._send_request("workspace/executeCommand", params)
384 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/dart/test_dart_basic.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | from pathlib import Path
  3 | 
  4 | import pytest
  5 | 
  6 | from solidlsp import SolidLanguageServer
  7 | from solidlsp.ls_config import Language
  8 | from solidlsp.ls_types import SymbolKind
  9 | from solidlsp.ls_utils import SymbolUtils
 10 | 
 11 | 
 12 | @pytest.mark.dart
 13 | class TestDartLanguageServer:
 14 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
 15 |     @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
 16 |     def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 17 |         """Test that the language server starts and stops successfully."""
 18 |         # The fixture already handles start and stop
 19 |         assert language_server.is_running()
 20 |         assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
 21 | 
 22 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
 23 |     @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
 24 |     def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 25 |         """Test finding definition of a method within the same file."""
 26 |         # In lib/main.dart:
 27 |         # Line 105: final result1 = calc.add(5, 3); // Reference to add method
 28 |         # Line 12: int add(int a, int b) {        // Definition of add method
 29 |         # Find definition of 'add' method from its usage
 30 |         main_dart_path = str(repo_path / "lib" / "main.dart")
 31 | 
 32 |         # Position: calc.add(5, 3) - cursor on 'add'
 33 |         # Line 105 (1-indexed) = line 104 (0-indexed), char position around 22
 34 |         definition_location_list = language_server.request_definition(main_dart_path, 104, 22)
 35 | 
 36 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
 37 |         assert len(definition_location_list) >= 1
 38 |         definition_location = definition_location_list[0]
 39 |         assert definition_location["uri"].endswith("main.dart")
 40 |         # Definition of add method should be around line 11 (0-indexed)
 41 |         # But language server may return different positions
 42 |         assert definition_location["range"]["start"]["line"] >= 0
 43 | 
 44 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
 45 |     @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
 46 |     def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 47 |         """Test finding definition across different files."""
 48 |         # Test finding definition of MathHelper class which is in helper.dart
 49 |         # In lib/main.dart line 50: MathHelper.power(step1, 2)
 50 |         main_dart_path = str(repo_path / "lib" / "main.dart")
 51 | 
 52 |         # Position: MathHelper.power(step1, 2) - cursor on 'MathHelper'
 53 |         # Line 50 (1-indexed) = line 49 (0-indexed), char position around 18
 54 |         definition_location_list = language_server.request_definition(main_dart_path, 49, 18)
 55 | 
 56 |         # Skip the test if language server doesn't find cross-file references
 57 |         # This is acceptable for a basic test - the important thing is that LS is working
 58 |         if not definition_location_list:
 59 |             pytest.skip("Language server doesn't support cross-file definition lookup for this case")
 60 | 
 61 |         assert len(definition_location_list) >= 1
 62 |         definition_location = definition_location_list[0]
 63 |         assert definition_location["uri"].endswith("helper.dart")
 64 |         assert definition_location["range"]["start"]["line"] >= 0
 65 | 
 66 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
 67 |     @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
 68 |     def test_find_definition_class_method(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 69 |         """Test finding definition of a class method."""
 70 |         # In lib/main.dart:
 71 |         # Line 50: final step2 = MathHelper.power(step1, 2); // Reference to MathHelper.power method
 72 |         # In lib/helper.dart:
 73 |         # Line 14: static double power(double base, int exponent) { // Definition of power method
 74 |         main_dart_path = str(repo_path / "lib" / "main.dart")
 75 | 
 76 |         # Position: MathHelper.power(step1, 2) - cursor on 'power'
 77 |         # Line 50 (1-indexed) = line 49 (0-indexed), char position around 30
 78 |         definition_location_list = language_server.request_definition(main_dart_path, 49, 30)
 79 | 
 80 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
 81 |         assert len(definition_location_list) >= 1
 82 |         definition_location = definition_location_list[0]
 83 |         assert definition_location["uri"].endswith("helper.dart")
 84 |         # Definition of power method should be around line 13 (0-indexed)
 85 |         assert 12 <= definition_location["range"]["start"]["line"] <= 16
 86 | 
 87 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
 88 |     @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
 89 |     def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
 90 |         """Test finding references to a method within the same file."""
 91 |         main_dart_path = str(repo_path / "lib" / "main.dart")
 92 | 
 93 |         # Find references to the 'add' method from its definition
 94 |         # Line 12: int add(int a, int b) { // Definition of add method
 95 |         # Line 105: final result1 = calc.add(5, 3); // Usage of add method
 96 |         references = language_server.request_references(main_dart_path, 11, 6)  # cursor on 'add' in definition
 97 | 
 98 |         assert references, f"Expected non-empty references but got {references=}"
 99 |         # Should find at least the usage of add method
100 |         assert len(references) >= 1
101 | 
102 |         # Check that we have a reference in main.dart
103 |         main_dart_references = [ref for ref in references if ref["uri"].endswith("main.dart")]
104 |         assert len(main_dart_references) >= 1
105 | 
106 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
107 |     @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
108 |     def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
109 |         """Test finding references across different files."""
110 |         helper_dart_path = str(repo_path / "lib" / "helper.dart")
111 | 
112 |         # Find references to the 'subtract' function from its definition in helper.dart
113 |         # Definition is in helper.dart, usage is in main.dart
114 |         references = language_server.request_references(helper_dart_path, 4, 4)  # cursor on 'subtract' in definition
115 | 
116 |         assert references, f"Expected non-empty references for subtract function but got {references=}"
117 | 
118 |         # Should find references in main.dart
119 |         main_dart_references = [ref for ref in references if ref["uri"].endswith("main.dart")]
120 |         assert len(main_dart_references) >= 1
121 | 
122 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
123 |     @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
124 |     def test_find_definition_constructor(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
125 |         """Test finding definition of a constructor call."""
126 |         main_dart_path = str(repo_path / "lib" / "main.dart")
127 | 
128 |         # In lib/main.dart:
129 |         # Line 104: final calc = Calculator(); // Reference to Calculator constructor
130 |         # Line 4: class Calculator {          // Definition of Calculator class
131 |         definition_location_list = language_server.request_definition(main_dart_path, 103, 18)  # cursor on 'Calculator'
132 | 
133 |         assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
134 |         assert len(definition_location_list) >= 1
135 |         definition_location = definition_location_list[0]
136 |         assert definition_location["uri"].endswith("main.dart")
137 |         # Definition of Calculator class should be around line 3 (0-indexed)
138 |         assert 3 <= definition_location["range"]["start"]["line"] <= 7
139 | 
140 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
141 |     @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
142 |     def test_find_definition_import(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
143 |         """Test finding definition through imports."""
144 |         models_dart_path = str(repo_path / "lib" / "models.dart")
145 | 
146 |         # Test finding definition of User class name where it's used
147 |         # In lib/models.dart line 27 (constructor): User(this.id, this.name, this.email, this._age);
148 |         definition_location_list = language_server.request_definition(models_dart_path, 26, 2)  # cursor on 'User' in constructor
149 | 
150 |         # Skip if language server doesn't find definition in this case
151 |         if not definition_location_list:
152 |             pytest.skip("Language server doesn't support definition lookup for this case")
153 | 
154 |         assert len(definition_location_list) >= 1
155 |         definition_location = definition_location_list[0]
156 |         # Language server might return SDK files instead of local files
157 |         # This is acceptable behavior - the important thing is that it found a definition
158 |         assert "dart" in definition_location["uri"].lower()
159 | 
160 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
161 |     def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
162 |         """Test finding symbols in the full symbol tree."""
163 |         symbols = language_server.request_full_symbol_tree()
164 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator class not found in symbol tree"
165 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add method not found in symbol tree"
166 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "subtract"), "subtract function not found in symbol tree"
167 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "MathHelper"), "MathHelper class not found in symbol tree"
168 |         assert SymbolUtils.symbol_tree_contains_name(symbols, "User"), "User class not found in symbol tree"
169 | 
170 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
171 |     def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
172 |         """Test finding references using symbol selection range."""
173 |         file_path = os.path.join("lib", "main.dart")
174 |         symbols = language_server.request_document_symbols(file_path)
175 | 
176 |         # Handle nested symbol structure - symbols can be nested in lists
177 |         symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
178 | 
179 |         # Find the 'add' method symbol in Calculator class
180 |         add_symbol = None
181 |         for sym in symbol_list:
182 |             if sym.get("name") == "add":
183 |                 add_symbol = sym
184 |                 break
185 |             # Check for nested symbols (methods inside classes)
186 |             if "children" in sym and sym.get("name") == "Calculator":
187 |                 for child in sym["children"]:
188 |                     if child.get("name") == "add":
189 |                         add_symbol = child
190 |                         break
191 |                 if add_symbol:
192 |                     break
193 | 
194 |         assert add_symbol is not None, "Could not find 'add' method symbol in main.dart"
195 |         sel_start = add_symbol["selectionRange"]["start"]
196 |         refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
197 | 
198 |         # Check that we found references - at least one should be in main.dart
199 |         assert any(
200 |             "main.dart" in ref.get("relativePath", "") or "main.dart" in ref.get("uri", "") for ref in refs
201 |         ), "main.dart should reference add method (tried all positions in selectionRange)"
202 | 
203 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
204 |     def test_request_containing_symbol_method(self, language_server: SolidLanguageServer) -> None:
205 |         """Test request_containing_symbol for a method."""
206 |         file_path = os.path.join("lib", "main.dart")
207 |         # Line 14 is inside the add method body (around 'final result = a + b;')
208 |         containing_symbol = language_server.request_containing_symbol(file_path, 13, 10, include_body=True)
209 | 
210 |         # Verify that we found the containing symbol
211 |         if containing_symbol is not None:
212 |             assert containing_symbol["name"] == "add"
213 |             assert containing_symbol["kind"] == SymbolKind.Method
214 |             if "body" in containing_symbol:
215 |                 assert "add" in containing_symbol["body"] or "final result" in containing_symbol["body"]
216 | 
217 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
218 |     def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
219 |         """Test request_containing_symbol for a class."""
220 |         file_path = os.path.join("lib", "main.dart")
221 |         # Line 4 is the Calculator class definition line
222 |         containing_symbol = language_server.request_containing_symbol(file_path, 4, 6)
223 | 
224 |         # Verify that we found the containing symbol
225 |         if containing_symbol is not None:
226 |             assert containing_symbol["name"] == "Calculator"
227 |             assert containing_symbol["kind"] == SymbolKind.Class
228 | 
229 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
230 |     def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
231 |         """Test request_containing_symbol with nested scopes."""
232 |         file_path = os.path.join("lib", "main.dart")
233 |         # Line 14 is inside the add method inside Calculator class
234 |         containing_symbol = language_server.request_containing_symbol(file_path, 13, 20)
235 | 
236 |         # Verify that we found the innermost containing symbol (the method)
237 |         if containing_symbol is not None:
238 |             assert containing_symbol["name"] == "add"
239 |             assert containing_symbol["kind"] == SymbolKind.Method
240 | 
241 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
242 |     def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
243 |         """Test request_defining_symbol for a variable usage."""
244 |         file_path = os.path.join("lib", "main.dart")
245 |         # Line 14 contains 'final result = a + b;' - test position on 'result'
246 |         defining_symbol = language_server.request_defining_symbol(file_path, 13, 10)
247 | 
248 |         # The defining symbol might be the variable itself or the containing method
249 |         # This is acceptable behavior - different language servers handle this differently
250 |         if defining_symbol is not None:
251 |             assert defining_symbol.get("name") in ["result", "add"]
252 |             if defining_symbol.get("name") == "add":
253 |                 assert defining_symbol.get("kind") == SymbolKind.Method.value
254 | 
255 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
256 |     def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
257 |         """Test request_defining_symbol for an imported class/function."""
258 |         file_path = os.path.join("lib", "main.dart")
259 |         # Line 20 references 'subtract' which was imported from helper.dart
260 |         defining_symbol = language_server.request_defining_symbol(file_path, 19, 18)
261 | 
262 |         # Verify that we found the defining symbol - this should be the subtract function from helper.dart
263 |         if defining_symbol is not None:
264 |             assert defining_symbol.get("name") == "subtract"
265 |             # Could be Function or Method depending on language server interpretation
266 |             assert defining_symbol.get("kind") in [SymbolKind.Function.value, SymbolKind.Method.value]
267 | 
268 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
269 |     def test_request_defining_symbol_class_method(self, language_server: SolidLanguageServer) -> None:
270 |         """Test request_defining_symbol for a static class method."""
271 |         file_path = os.path.join("lib", "main.dart")
272 |         # Line 50 references MathHelper.power - test position on 'power'
273 |         defining_symbol = language_server.request_defining_symbol(file_path, 49, 30)
274 | 
275 |         # Verify that we found the defining symbol - should be the power method
276 |         if defining_symbol is not None:
277 |             assert defining_symbol.get("name") == "power"
278 |             assert defining_symbol.get("kind") == SymbolKind.Method.value
279 | 
280 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
281 |     def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
282 |         """Test getting document symbols from a Dart file."""
283 |         file_path = os.path.join("lib", "main.dart")
284 |         symbols = language_server.request_document_symbols(file_path)
285 | 
286 |         # Check that we have symbols
287 |         assert len(symbols) > 0
288 | 
289 |         # Flatten the symbols if they're nested
290 |         symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
291 | 
292 |         # Look for expected classes and methods
293 |         symbol_names = [s.get("name") for s in symbol_list]
294 |         assert "Calculator" in symbol_names
295 | 
296 |         # Check for nested symbols (methods inside classes) - optional
297 |         calculator_symbol = next((s for s in symbol_list if s.get("name") == "Calculator"), None)
298 |         if calculator_symbol and "children" in calculator_symbol and calculator_symbol["children"]:
299 |             method_names = [child.get("name") for child in calculator_symbol["children"]]
300 |             # If children are populated, we should find the add method
301 |             assert "add" in method_names
302 |         else:
303 |             # Some language servers may not populate children in document symbols
304 |             # This is acceptable behavior - the important thing is we found the class
305 |             pass
306 | 
307 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
308 |     def test_request_referencing_symbols_comprehensive(self, language_server: SolidLanguageServer) -> None:
309 |         """Test comprehensive referencing symbols functionality."""
310 |         file_path = os.path.join("lib", "main.dart")
311 |         symbols = language_server.request_document_symbols(file_path)
312 | 
313 |         # Handle nested symbol structure
314 |         symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
315 | 
316 |         # Find Calculator class and test its references
317 |         calculator_symbol = None
318 |         for sym in symbol_list:
319 |             if sym.get("name") == "Calculator":
320 |                 calculator_symbol = sym
321 |                 break
322 | 
323 |         if calculator_symbol and "selectionRange" in calculator_symbol:
324 |             sel_start = calculator_symbol["selectionRange"]["start"]
325 |             refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
326 | 
327 |             # Should find references to Calculator (constructor calls, etc.)
328 |             if refs:
329 |                 # Verify the structure of referencing symbols
330 |                 for ref in refs:
331 |                     assert "uri" in ref or "relativePath" in ref
332 |                     if "range" in ref:
333 |                         assert "start" in ref["range"]
334 |                         assert "end" in ref["range"]
335 | 
336 |     @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
337 |     def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer) -> None:
338 |         """Test symbol resolution across multiple files."""
339 |         helper_file_path = os.path.join("lib", "helper.dart")
340 | 
341 |         # Test finding references to subtract function from helper.dart in main.dart
342 |         helper_symbols = language_server.request_document_symbols(helper_file_path)
343 |         symbol_list = helper_symbols[0] if helper_symbols and isinstance(helper_symbols[0], list) else helper_symbols
344 | 
345 |         subtract_symbol = next((s for s in symbol_list if s.get("name") == "subtract"), None)
346 | 
347 |         if subtract_symbol and "selectionRange" in subtract_symbol:
348 |             sel_start = subtract_symbol["selectionRange"]["start"]
349 |             refs = language_server.request_references(helper_file_path, sel_start["line"], sel_start["character"])
350 | 
351 |             # Should find references in main.dart
352 |             main_dart_refs = [ref for ref in refs if "main.dart" in ref.get("uri", "") or "main.dart" in ref.get("relativePath", "")]
353 |             # Note: This may not always work depending on language server capabilities
354 |             # So we don't assert - just verify the structure if we get results
355 |             if main_dart_refs:
356 |                 for ref in main_dart_refs:
357 |                     assert "range" in ref or "location" in ref
358 | 
```
Page 8/14FirstPrevNextLast