Refactoring: Improve dependency structure of LanguageServerSymbolRetriever & CodeEditors

Simplify dependency injections by depending on Project instead of low-level instances
* LanguageServerSymbolRetriever(Project) - contains the instance
* LanguageServerCodeEditor(LanguageServerSymbolRetriever)
* CodeEditor(Project)

Move retrieval methods for LS manager from SerenaAgent to Project, including
the exception-based problem report
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-03-09 23:20:45 +01:00
1 parent 6bf71ef10b
commit 65f5145ab0
8 files changed
+133 -170

No files matched your search

+5 -20
View File
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Optional, TypeVar
from sensai.util import logging
from sensai.util.logging import LogTime
from sensai.util.string import TextBuilder, dict_string
from sensai.util.string import dict_string
from interprompt.jinja_template import JinjaTemplate
from serena import serena_version
@@ -29,7 +29,7 @@ from serena.config.serena_config import (
ToolInclusionDefinition,
)
from serena.dashboard import SerenaDashboardAPI
from serena.ls_manager import LanguageServerManager, LanguageServerManagerInitialisationError
from serena.ls_manager import LanguageServerManager
from serena.project import MemoriesManager, Project
from serena.prompt_factory import SerenaPromptFactory
from serena.task_executor import TaskExecutor
@@ -255,7 +255,6 @@ class SerenaAgent:
# project-specific instances, which will be initialized upon project activation
self._active_project: Project | None = None
self._lsp_init_error: LanguageServerManagerInitialisationError | None = None
# determine registered project to be activated (if any)
registered_project_to_activate: RegisteredProject | None = (
@@ -475,17 +474,8 @@ class SerenaAgent:
return None
def get_language_server_manager_or_raise(self) -> LanguageServerManager:
language_server_manager = self.get_language_server_manager()
if language_server_manager is None:
msg = TextBuilder("The language server manager is not initialized, indicating a problem during project initialisation.")
if self._lsp_init_error is not None:
msg.with_text(str(self._lsp_init_error))
msg.with_text("For details, please check the logs. " + self.get_log_inspection_instructions())
msg.with_text(
"IMPORTANT: Stop, do not attempt workarounds. Inform the user and wait for further instructions before you continue!"
)
raise Exception(msg.build())
return language_server_manager
active_project = self.get_active_project_or_raise()
return active_project.get_language_server_manager_or_raise()
def get_log_inspection_instructions(self) -> str:
if self.serena_config.web_dashboard:
@@ -820,12 +810,7 @@ class SerenaAgent:
"""
Starts/resets the language server manager for the current project
"""
try:
self._lsp_init_error = None
self.get_active_project_or_raise().create_language_server_manager()
except LanguageServerManagerInitialisationError as e:
self._lsp_init_error = e
raise
self.get_active_project_or_raise().create_language_server_manager()
def add_language(self, language: Language) -> None:
"""
+7 -13
View File
@@ -6,14 +6,12 @@ from collections.abc import Iterable, Iterator, Reversible
from contextlib import contextmanager
from typing import Generic, TypeVar, cast
from serena.config.serena_config import ProjectConfig
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient
from serena.symbol import JetBrainsSymbol, LanguageServerSymbol, LanguageServerSymbolRetriever, PositionInFile, Symbol
from solidlsp import SolidLanguageServer, ls_types
from solidlsp.ls import LSPFileBuffer
from solidlsp.ls_utils import PathUtils, TextUtils
from .constants import DEFAULT_SOURCE_FILE_ENCODING
from .project import Project
log = logging.getLogger(__name__)
@@ -21,12 +19,10 @@ TSymbol = TypeVar("TSymbol", bound=Symbol)
class CodeEditor(Generic[TSymbol], ABC):
def __init__(self, project_root: str, project_config: ProjectConfig | None = None, newline: str | None = None) -> None:
self.project_root = project_root
# set encoding based on project configuration, if available
self.encoding = project_config.encoding if project_config is not None else DEFAULT_SOURCE_FILE_ENCODING
self.newline = newline
def __init__(self, project: Project) -> None:
self.project_root = project.project_root
self.encoding = project.project_config.encoding
self.newline = project.line_ending.newline_str
class EditedFile(ABC):
def __init__(self, relative_path: str) -> None:
@@ -236,10 +232,8 @@ class CodeEditor(Generic[TSymbol], ABC):
class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]):
def __init__(
self, symbol_retriever: LanguageServerSymbolRetriever, project_config: ProjectConfig | None = None, newline: str | None = None
):
super().__init__(project_root=symbol_retriever.get_root_path(), project_config=project_config, newline=newline)
def __init__(self, symbol_retriever: LanguageServerSymbolRetriever):
super().__init__(project=symbol_retriever.project)
self._symbol_retriever = symbol_retriever
def _get_language_server(self, relative_path: str) -> SolidLanguageServer:
@@ -375,7 +369,7 @@ class LanguageServerCodeEditor(CodeEditor[LanguageServerSymbol]):
class JetBrainsCodeEditor(CodeEditor[JetBrainsSymbol]):
def __init__(self, project: Project) -> None:
self._project = project
super().__init__(project_root=project.project_root, project_config=project.project_config, newline=project.line_ending.newline_str)
super().__init__(project)
class EditedFile(CodeEditor.EditedFile):
def __init__(self, relative_path: str, project: Project):
+45 -27
View File
@@ -10,7 +10,7 @@ from typing import Any, Literal
import pathspec
from sensai.util.logging import LogTime
from sensai.util.string import ToStringMixin
from sensai.util.string import TextBuilder, ToStringMixin
from serena.config.serena_config import (
ProjectConfig,
@@ -242,7 +242,7 @@ class Project(ToStringMixin):
assert serena_config is not None
self.project_root = project_root
self.project_config = project_config
self._serena_config = serena_config
self.serena_config = serena_config
self._serena_data_folder = serena_config.get_project_serena_folder(self.project_root)
log.info("Serena project data folder: %s", self._serena_data_folder)
@@ -253,6 +253,7 @@ class Project(ToStringMixin):
self.line_ending = project_config.line_ending or serena_config.line_ending
self.language_server_manager: LanguageServerManager | None = None
self._language_server_manager_init_error: Exception | None = None
self._is_newly_created = is_newly_created
# create .gitignore file in the project's Serena data folder if not yet present
@@ -274,7 +275,7 @@ class Project(ToStringMixin):
with LogTime(f"Gathering ignore spec for project {self.project_config.project_name}", logger=log):
# gather ignored paths from the global configuration, project configuration, and gitignore files
global_ignored_paths = self._serena_config.ignored_paths
global_ignored_paths = self.serena_config.ignored_paths
ignored_patterns = list(global_ignored_paths) + list(self.project_config.ignored_paths)
if len(global_ignored_paths) > 0:
log.info(f"Using {len(global_ignored_paths)} ignored paths from the global configuration.")
@@ -598,32 +599,49 @@ class Project(ToStringMixin):
:return: the language server manager, which is also stored in the project instance
"""
# determine timeout to use for LS calls
tool_timeout = self._serena_config.tool_timeout
if tool_timeout is None or tool_timeout < 0:
ls_timeout = None
else:
if tool_timeout < 10:
raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds")
ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout
try:
# determine timeout to use for LS calls
tool_timeout = self.serena_config.tool_timeout
if tool_timeout is None or tool_timeout < 0:
ls_timeout = None
else:
if tool_timeout < 10:
raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds")
ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout
# if there is an existing instance, stop its language servers first
if self.language_server_manager is not None:
log.info("Stopping existing language server manager ...")
self.language_server_manager.stop_all()
self.language_server_manager = None
# if there is an existing instance, stop its language servers first
if self.language_server_manager is not None:
log.info("Stopping existing language server manager ...")
self.language_server_manager.stop_all()
self.language_server_manager = None
log.info(f"Creating language server manager for {self.project_root}")
factory = LanguageServerFactory(
project_root=self.project_root,
project_data_path=self._serena_data_folder,
encoding=self.project_config.encoding,
ignored_patterns=self._ignored_patterns,
ls_timeout=ls_timeout,
ls_specific_settings=self._serena_config.ls_specific_settings,
trace_lsp_communication=self._serena_config.trace_lsp_communication,
)
self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory)
log.info(f"Creating language server manager for {self.project_root}")
self._language_server_manager_init_error = None
factory = LanguageServerFactory(
project_root=self.project_root,
project_data_path=self._serena_data_folder,
encoding=self.project_config.encoding,
ignored_patterns=self._ignored_patterns,
ls_timeout=ls_timeout,
ls_specific_settings=self.serena_config.ls_specific_settings,
trace_lsp_communication=self.serena_config.trace_lsp_communication,
)
self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory)
return self.language_server_manager
except Exception as e:
self._language_server_manager_init_error = e
raise
def get_language_server_manager_or_raise(self) -> LanguageServerManager:
if self.language_server_manager is None:
msg = TextBuilder("The language server manager is not initialized, indicating a problem during project initialisation.")
if self._language_server_manager_init_error is not None:
msg.with_text(str(self._lsp_init_error))
msg.with_text("For details, please check the logs. " + self.get_log_inspection_instructions())
msg.with_text(
"IMPORTANT: Stop, do not attempt workarounds. Inform the user and wait for further instructions before you continue!"
)
raise Exception(msg.build())
return self.language_server_manager
def add_language(self, language: Language) -> None:
+10 -25
View File
@@ -5,7 +5,7 @@ from abc import ABC, abstractmethod
from collections.abc import Callable, Iterator, Sequence
from dataclasses import asdict, dataclass
from time import perf_counter
from typing import TYPE_CHECKING, Any, Generic, Literal, NotRequired, Self, TypedDict, TypeVar, Union
from typing import Any, Generic, Literal, NotRequired, Self, TypedDict, TypeVar
from sensai.util.string import ToStringMixin
@@ -18,9 +18,6 @@ from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation
from .ls_manager import LanguageServerManager
from .project import Project
if TYPE_CHECKING:
from .agent import SerenaAgent
log = logging.getLogger(__name__)
NAME_PATH_SEP = "/"
@@ -536,19 +533,12 @@ class ReferenceInLanguageServerSymbol(ToStringMixin):
class LanguageServerSymbolRetriever:
def __init__(self, ls: SolidLanguageServer | LanguageServerManager, agent: Union["SerenaAgent", None] = None) -> None:
def __init__(self, project: Project) -> None:
"""
:param ls: the language server or language server manager to use for symbol retrieval and editing operations.
:param agent: the agent to use (only needed for marking files as modified). You can pass None if you don't
need an agent to be aware of file modifications performed by the symbol manager.
:param project: the project instance
"""
if isinstance(ls, SolidLanguageServer):
ls_manager = LanguageServerManager({ls.language: ls})
else:
ls_manager = ls
assert isinstance(ls_manager, LanguageServerManager)
self._ls_manager: LanguageServerManager = ls_manager
self.agent = agent
self._ls_manager: LanguageServerManager = project.get_language_server_manager_or_raise()
self.project = project
def _request_info(self, relative_file_path: str, line: int, column: int, file_buffer: LSPFileBuffer | None = None) -> str | None:
"""Retrieves information (in a sanitized format) about the symbol at the desired location,
@@ -588,16 +578,11 @@ class LanguageServerSymbolRetriever:
return None
return self._request_info(relative_file_path=symbol.relative_path, line=symbol.line, column=symbol.column) # type: ignore[arg-type]
def _get_symbol_info_budget(self, default_budget: float = 10) -> float:
"""Project -> global -> default"""
symbol_info_budget = default_budget
if self.agent is not None:
symbol_info_budget = self.agent.serena_config.symbol_info_budget
active_project = self.agent.get_active_project()
if active_project is not None:
project_symbol_info_budget = active_project.project_config.symbol_info_budget
if project_symbol_info_budget is not None:
symbol_info_budget = project_symbol_info_budget
def _get_symbol_info_budget(self) -> float:
symbol_info_budget = self.project.serena_config.symbol_info_budget
project_symbol_info_budget = self.project.project_config.symbol_info_budget
if project_symbol_info_budget is not None:
symbol_info_budget = project_symbol_info_budget
return symbol_info_budget
def request_info_for_symbol_batch(
+2 -7
View File
@@ -51,8 +51,7 @@ class Component(ABC):
from serena.symbol import LanguageServerSymbolRetriever
assert self.agent.get_language_backend().is_lsp(), "Language server symbol retriever can only be created for LSP language backend"
language_server_manager = self.agent.get_language_server_manager_or_raise()
return LanguageServerSymbolRetriever(language_server_manager, agent=self.agent)
return LanguageServerSymbolRetriever(self.project)
@property
def project(self) -> Project:
@@ -63,11 +62,7 @@ class Component(ABC):
match self.agent.get_language_backend():
case LanguageBackend.LSP:
return LanguageServerCodeEditor(
self.create_language_server_symbol_retriever(),
project_config=self.project.project_config,
newline=self.project.line_ending.newline_str,
)
return LanguageServerCodeEditor(self.create_language_server_symbol_retriever())
case LanguageBackend.JETBRAINS:
return JetBrainsCodeEditor(project=self.project)
case _:
+32 -7
View File
@@ -120,8 +120,8 @@ def create_default_serena_config():
return SerenaConfig(gui_log_window=False, web_dashboard=False)
def _create_default_project(language: Language) -> Project:
repo_path = str(get_repo_path(language))
def _create_default_project(language: Language, repo_root_override: str | None = None) -> Project:
repo_path = str(get_repo_path(language)) if repo_root_override is None else repo_root_override
return Project.load(repo_path, serena_config=create_default_serena_config())
@@ -178,8 +178,18 @@ def language_server(request: LanguageParamRequest):
yield ls
@contextmanager
def project_context(language: Language, repo_root_override: str | None = None) -> Iterator[Project]:
"""Context manager that creates a Project for the specified language and ensures proper cleanup."""
project = _create_default_project(language, repo_root_override)
try:
yield project
finally:
project.shutdown(timeout=5)
@pytest.fixture(scope="module")
def project(request: LanguageParamRequest):
def project(request: LanguageParamRequest, repo_root_override: str | None = None) -> Iterator[Project]:
"""Create a Project for the specified language.
This fixture requires a language parameter via pytest.mark.parametrize:
@@ -203,11 +213,26 @@ def project(request: LanguageParamRequest):
"""
if not hasattr(request, "param"):
raise ValueError("Language parameter must be provided via pytest.mark.parametrize")
language = request.param
project = _create_default_project(language)
yield project
project.shutdown(timeout=5)
with project_context(language, repo_root_override) as project:
yield project
@contextmanager
def project_with_ls_context(language: Language, repo_root_override: str | None = None) -> Iterator[Project]:
"""Context manager that creates a Project with an active language server for the specified language."""
with project_context(language, repo_root_override) as project:
project.create_language_server_manager()
yield project
@pytest.fixture(scope="module")
def project_with_ls(request: LanguageParamRequest) -> Iterator[Project]:
if not hasattr(request, "param"):
raise ValueError("Language parameter must be provided via pytest.mark.parametrize")
language = request.param
with project_with_ls_context(language) as project:
yield project
is_ci = os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"
+29 -67
View File
@@ -3,8 +3,8 @@ from unittest.mock import MagicMock
import pytest
from serena.jetbrains.jetbrains_types import SymbolDTO, SymbolDTOKey
from serena.project import Project
from serena.symbol import LanguageServerSymbol, LanguageServerSymbolRetriever, NamePathComponent, NamePathMatcher
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
@@ -235,9 +235,9 @@ class TestSymbolNameMatching:
@pytest.mark.python
class TestLanguageServerSymbolRetriever:
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
def test_request_info(self, language_server: SolidLanguageServer):
symbol_retriever = LanguageServerSymbolRetriever(language_server)
@pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True)
def test_request_info(self, project_with_ls: Project):
symbol_retriever = LanguageServerSymbolRetriever(project_with_ls)
create_user_method_symbol = symbol_retriever.find("UserService/create_user", within_relative_path="test_repo/services.py")[0]
create_user_method_symbol_info = symbol_retriever.request_info_for_symbol(create_user_method_symbol)
assert "Create a new user and store it" in create_user_method_symbol_info
@@ -280,15 +280,14 @@ def _make_mock_symbols(count: int, *, relative_path: str = "test_repo/services.p
class TestHoverBudget:
"""Tests for symbol_info_budget time budget behavior."""
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
def test_budget_not_exceeded_all_lookups_performed(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch):
@pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True)
def test_budget_not_exceeded_all_lookups_performed(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch):
"""With a large budget, all hover lookups are performed."""
# Create symbol retriever with a mock agent that has large budget
mock_agent = MagicMock()
mock_agent.serena_config.symbol_info_budget = 10.0
mock_agent.get_active_project.return_value = None
project_with_ls.serena_config.symbol_info_budget = 10.0
project_with_ls.project_config.symbol_info_budget = 10.0
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
symbol_retriever = LanguageServerSymbolRetriever(project_with_ls)
# Track _request_info calls
call_count = 0
@@ -310,15 +309,13 @@ class TestHoverBudget:
assert all(info is not None for info in result.values())
assert len(result) == 3
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
def test_budget_exceeded_partial_info(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch):
@pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True)
def test_budget_exceeded_partial_info(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch):
"""With a small budget, hover lookups stop and remaining symbols get None info."""
# Create symbol retriever with a mock agent that has small budget (0.1s)
mock_agent = MagicMock()
mock_agent.serena_config.symbol_info_budget = 0.1
mock_agent.get_active_project.return_value = None
project_with_ls.serena_config.symbol_info_budget = 0.1
project_with_ls.project_config.symbol_info_budget = 0.1
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
symbol_retriever = LanguageServerSymbolRetriever(project_with_ls)
# Track _request_info calls and simulate 0.05s per call
call_count = 0
@@ -356,15 +353,13 @@ class TestHoverBudget:
assert result_list[3] is None
assert result_list[4] is None
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
def test_budget_zero_means_unlimited(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch):
@pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True)
def test_budget_zero_means_unlimited(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch):
"""With budget=0, all hover lookups proceed (no early stopping)."""
# Create symbol retriever with budget=0 (unlimited)
mock_agent = MagicMock()
mock_agent.serena_config.symbol_info_budget = 0.0
mock_agent.get_active_project.return_value = None
project_with_ls.serena_config.symbol_info_budget = 0.0
project_with_ls.project_config.symbol_info_budget = 0.0
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
symbol_retriever = LanguageServerSymbolRetriever(project_with_ls)
# Track _request_info calls
call_count = 0
@@ -385,18 +380,14 @@ class TestHoverBudget:
assert call_count == 5
assert all(info is not None for info in result.values())
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
def test_project_budget_overrides_global(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch):
@pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True)
def test_project_budget_overrides_global(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch):
"""Project-level budget overrides global budget."""
# Create symbol retriever with global budget 10.0 but project budget 0.05
mock_project = MagicMock()
mock_project.project_config.symbol_info_budget = 0.05
project_with_ls.project_config.symbol_info_budget = 0.05
project_with_ls.serena_config.symbol_info_budget = 10.0
mock_agent = MagicMock()
mock_agent.serena_config.symbol_info_budget = 10.0
mock_agent.get_active_project.return_value = mock_project
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
symbol_retriever = LanguageServerSymbolRetriever(project_with_ls)
# Track _request_info calls and simulate time
call_count = 0
@@ -427,18 +418,14 @@ class TestHoverBudget:
# So 2 calls succeed (proving project budget 0.05 overrode global 10.0)
assert call_count == 2
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
def test_project_null_inherits_global(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch):
@pytest.mark.parametrize("project_with_ls", [Language.PYTHON], indirect=True)
def test_project_null_inherits_global(self, project_with_ls: Project, monkeypatch: pytest.MonkeyPatch):
"""When project budget is None, global budget is used."""
# Create symbol retriever with project budget=None (inherit global)
mock_project = MagicMock()
mock_project.project_config.symbol_info_budget = None
project_with_ls.project_config.symbol_info_budget = None
project_with_ls.serena_config.symbol_info_budget = 10.0
mock_agent = MagicMock()
mock_agent.serena_config.symbol_info_budget = 10.0
mock_agent.get_active_project.return_value = mock_project
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
symbol_retriever = LanguageServerSymbolRetriever(project_with_ls)
# Track _request_info calls
call_count = 0
@@ -458,28 +445,3 @@ class TestHoverBudget:
# Global budget is 10s, all 3 should succeed
assert call_count == 3
assert all(info is not None for info in result.values())
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
def test_no_agent_uses_default_budget(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch):
"""When agent is None, default budget of 5s is used."""
# Create symbol retriever without agent
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=None)
# Track _request_info calls
call_count = 0
def counting_request_info(file_path, line, column, **kwargs):
nonlocal call_count
call_count += 1
return f"info:{line}:{column}"
monkeypatch.setattr(symbol_retriever, "_request_info", counting_request_info)
# Create 3 mock symbols
symbols = _make_mock_symbols(3)
result = symbol_retriever.request_info_for_symbol_batch(symbols)
# Default budget is 5s, all 3 should succeed
assert call_count == 3
assert all(info is not None for info in result.values())
+3 -4
View File
@@ -24,7 +24,7 @@ from syrupy import SnapshotAssertion
from serena.code_editor import CodeEditor, LanguageServerCodeEditor
from solidlsp.ls_config import Language
from src.serena.symbol import LanguageServerSymbolRetriever
from test.conftest import get_repo_path, start_ls_context
from test.conftest import get_repo_path, project_with_ls_context
pytestmark = pytest.mark.snapshot
@@ -192,7 +192,6 @@ class EditingTest(ABC):
"""Context manager for setup/teardown with a temporary directory, providing the symbol manager."""
temp_dir = Path(tempfile.mkdtemp())
self.repo_path = temp_dir / self.original_repo_path.name
language_server = None # Initialize language_server
try:
print(f"Copying repo from {self.original_repo_path} to {self.repo_path}")
shutil.copytree(self.original_repo_path, self.repo_path)
@@ -201,8 +200,8 @@ class EditingTest(ABC):
if os.name == "nt":
time.sleep(0.1)
log.info(f"Creating language server for {self.language} {self.rel_path}")
with start_ls_context(self.language, str(self.repo_path)) as language_server:
yield LanguageServerSymbolRetriever(ls=language_server)
with project_with_ls_context(self.language, str(self.repo_path)) as project:
yield LanguageServerSymbolRetriever(project)
finally:
# prevent deadlock on Windows due to lingering file locks
if os.name == "nt":