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 | ```