mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-03 07:13:33 +00:00
Reify language backends, introducing LanguageBackendRegistry
* Many case differentiations in the agent code were replaced by method calls in the newly introduced LanguageBackend abstraction * The registry allows new backends to be added dynamically (via Python packages that implement a specific entrypoint)
This commit is contained in:
1 parent
3ad4044c69
commit
3b898b671c
27 files changed
+571
-262
No files matched your search
@@ -192,7 +192,7 @@ def autogen_tool_list(target_filename = "01-about/035_tools.md"):
|
||||
def facades_section():
|
||||
from serena.repl.facade import ApiScope
|
||||
from serena.agent import SerenaAgent, SerenaConfig
|
||||
from serena.config.serena_config import LanguageBackend
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
|
||||
f.write("\n\n## Serena's REPL (Code Execution-Based Interface)\n\n")
|
||||
f.write(
|
||||
@@ -236,8 +236,8 @@ def autogen_tool_list(target_filename = "01-about/035_tools.md"):
|
||||
api_scope = ApiScope()
|
||||
agent = SerenaAgent(serena_config=SerenaConfig().with_headless_mode_overrides())
|
||||
facades = []
|
||||
for backend in LanguageBackend:
|
||||
facades.extend(backend.create_facades(agent, api_scope))
|
||||
for backend in BuiltinLanguageBackend:
|
||||
facades.extend(backend.get_instance().create_facades(agent, api_scope))
|
||||
facades.extend(agent.create_default_facade_list(api_scope))
|
||||
|
||||
text = TextBuilder()
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ packages = ["src/serena", "src/interprompt", "src/solidlsp"]
|
||||
max-line-length = 1000
|
||||
|
||||
[tool.ty.environment]
|
||||
python-version = "3.11"
|
||||
python-version = "3.11" # We configure the oldest Python version supported by Serena
|
||||
# Analyze for all platforms rather than defaulting to the OS ty happens to run on. This keeps the
|
||||
# check deterministic across the CI matrix (Linux/Windows/macOS) and lets platform-conditional stdlib
|
||||
# members (e.g. subprocess.CREATE_NO_WINDOW, ctypes.windll, pwd) resolve without per-OS type-ignores.
|
||||
|
||||
@@ -14,8 +14,9 @@ from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
from serena.project import Project
|
||||
from serena.tools import (
|
||||
CreateTextFileTool,
|
||||
@@ -35,7 +36,7 @@ def make_agent() -> SerenaAgent:
|
||||
"""Create an LSP-backed Serena agent for the Serena repository."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
serena_config.set_builtin_language_backend(BuiltinLanguageBackend.LSP)
|
||||
|
||||
project = Project(
|
||||
project_root=str(REPO_PATH),
|
||||
|
||||
@@ -9,8 +9,9 @@ from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
from serena.project import Project
|
||||
from serena.tools import FindDeclarationTool
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
@@ -24,7 +25,7 @@ def make_agent(project_root: Path, language: LanguageServerId, project_name: str
|
||||
"""Create an LSP-backed Serena agent for a single explicit project."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
serena_config.set_builtin_language_backend(BuiltinLanguageBackend.LSP)
|
||||
|
||||
project = Project(
|
||||
project_root=str(project_root),
|
||||
|
||||
@@ -8,8 +8,9 @@ from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
from serena.project import Project
|
||||
from serena.tools import FindImplementationsTool
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
@@ -22,7 +23,7 @@ def make_agent(project_root: Path, language: LanguageServerId, project_name: str
|
||||
"""Create an LSP-backed Serena agent for a single explicit project."""
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
serena_config.set_builtin_language_backend(BuiltinLanguageBackend.LSP)
|
||||
|
||||
project = Project(
|
||||
project_root=str(project_root),
|
||||
|
||||
@@ -11,8 +11,9 @@ import json
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, SerenaConfig
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
from serena.tools import (
|
||||
FindReferencingSymbolsTool,
|
||||
FindSymbolTool,
|
||||
@@ -165,16 +166,16 @@ def run_jb_tools(agent: SerenaAgent) -> None:
|
||||
)
|
||||
|
||||
|
||||
def make_agent(backend: LanguageBackend) -> SerenaAgent:
|
||||
def make_agent(backend: BuiltinLanguageBackend) -> SerenaAgent:
|
||||
config = SerenaConfig.from_config_file()
|
||||
config.web_dashboard = False
|
||||
config.language_backend = backend
|
||||
config.set_builtin_language_backend(backend)
|
||||
return SerenaAgent(project=REPO_ROOT, serena_config=config)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# LSP backend
|
||||
lsp_agent = make_agent(LanguageBackend.LSP)
|
||||
lsp_agent = make_agent(BuiltinLanguageBackend.LSP)
|
||||
try:
|
||||
run_lsp_tools(lsp_agent)
|
||||
run_backend_independent_tools(lsp_agent)
|
||||
@@ -183,7 +184,7 @@ if __name__ == "__main__":
|
||||
|
||||
# JetBrains backend (requires a running IDE)
|
||||
try:
|
||||
jb_agent = make_agent(LanguageBackend.JETBRAINS)
|
||||
jb_agent = make_agent(BuiltinLanguageBackend.JETBRAINS)
|
||||
try:
|
||||
run_jb_tools(jb_agent)
|
||||
finally:
|
||||
|
||||
@@ -9,8 +9,9 @@ from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import LanguageBackend, SerenaConfig
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.constants import REPO_ROOT
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
from serena.tools import (
|
||||
FindFileTool,
|
||||
FindReferencingSymbolsTool,
|
||||
@@ -26,7 +27,7 @@ from serena.tools import (
|
||||
if __name__ == "__main__":
|
||||
serena_config = SerenaConfig.from_config_file()
|
||||
serena_config.web_dashboard = False
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
serena_config.set_builtin_language_backend(BuiltinLanguageBackend.LSP)
|
||||
# project = Path(REPO_ROOT).parent / "serena-jetbrains-plugin-copy"
|
||||
project = Path(REPO_ROOT)
|
||||
agent = SerenaAgent(project=str(project), serena_config=serena_config)
|
||||
|
||||
+12
-39
@@ -32,7 +32,6 @@ from serena.analytics import RegisteredTokenCountEstimator, ToolUsageStats
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.config.serena_config import (
|
||||
AgentInterface,
|
||||
LanguageBackend,
|
||||
ModeSelectionDefinition,
|
||||
ModeSelectionDefinitionWithAddedModes,
|
||||
ModeSelectionDefinitionWithBaseModes,
|
||||
@@ -44,7 +43,7 @@ from serena.config.serena_config import (
|
||||
ToolInclusionDefinition,
|
||||
)
|
||||
from serena.dashboard import SerenaDashboardAPI, SerenaDashboardTrayManager, SerenaDashboardViewer, open_url_in_browser
|
||||
from serena.jetbrains import launch_coordinator as jetbrains_launch_coordinator
|
||||
from serena.language_backend import BuiltinLanguageBackend, LanguageBackend
|
||||
from serena.ls_manager import LanguageServerManager
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
from serena.project import Project
|
||||
@@ -756,7 +755,7 @@ class SerenaAgent:
|
||||
"os": platform.system(),
|
||||
"dashboard": int(self.serena_config.web_dashboard),
|
||||
"version": self.version,
|
||||
"backend": self._language_backend.value,
|
||||
"backend": self._language_backend.get_key(),
|
||||
"context": self._context.name,
|
||||
}
|
||||
try:
|
||||
@@ -1226,9 +1225,7 @@ class SerenaAgent:
|
||||
msg = f"Created and activated a new project with name '{proj.project_name}' at {proj.project_root}.\n"
|
||||
else:
|
||||
msg = f"The project with name '{proj.project_name}' at {proj.project_root} is activated.\n"
|
||||
if self._language_backend == LanguageBackend.LSP:
|
||||
language_servers_str = ", ".join([ls.get_key() for ls in proj.project_config.language_servers])
|
||||
msg += f"Active language servers: {language_servers_str}.\n"
|
||||
msg += self._language_backend.get_project_activation_statement(proj)
|
||||
msg += f"File encoding: {proj.project_config.encoding}.\n"
|
||||
|
||||
# add list of memories (if memories are enabled)
|
||||
@@ -1405,7 +1402,7 @@ class SerenaAgent:
|
||||
"""
|
||||
:return: whether this agent uses language server-based code analysis
|
||||
"""
|
||||
return self._language_backend == LanguageBackend.LSP
|
||||
return self._language_backend == BuiltinLanguageBackend.LSP
|
||||
|
||||
def _activate_project(self, project: Project, update_active_modes: bool = True, update_active_tools: bool = True) -> bool:
|
||||
"""
|
||||
@@ -1428,14 +1425,12 @@ class SerenaAgent:
|
||||
if project_backend is not None and project_backend != self._language_backend:
|
||||
if self._agent_interface.is_tools():
|
||||
raise ValueError(
|
||||
f"Cannot activate project '{project.project_name}': it requires the {project_backend.value} backend, "
|
||||
f"but this session was initialized with {self._language_backend.value}. "
|
||||
f"Cannot activate project '{project.project_name}': it requires the {project_backend} backend, "
|
||||
f"but this session was initialized with {self._language_backend}. "
|
||||
f"Workarounds: (1) Use project activation at startup via the --project flag, "
|
||||
f"(2) Configure one MCP server per backend in your client, (3) use the REPL interface."
|
||||
)
|
||||
log.info(
|
||||
f"Switching language backend from {self._language_backend.value} to {project_backend.value} for project '{project.project_name}'"
|
||||
)
|
||||
log.info(f"Switching language backend from {self._language_backend} to {project_backend} for project '{project.project_name}'")
|
||||
self._language_backend = project_backend
|
||||
|
||||
# shut down the previously active project to release its language server processes
|
||||
@@ -1460,7 +1455,7 @@ class SerenaAgent:
|
||||
|
||||
def init_project_services() -> None:
|
||||
self._run_project_activation_command(project)
|
||||
self._init_active_project_language_backend()
|
||||
self._language_backend.init_active_project(self)
|
||||
|
||||
# initialise the project's language backend in the background
|
||||
self.issue_task(init_project_services)
|
||||
@@ -1516,28 +1511,6 @@ class SerenaAgent:
|
||||
except Exception:
|
||||
log.exception(f"Unexpected error running activation_command for project '{project.project_name}'")
|
||||
|
||||
def _init_active_project_language_backend(self) -> None:
|
||||
"""
|
||||
Initialises the active project's language backend
|
||||
"""
|
||||
project = self._active_project
|
||||
assert project is not None
|
||||
|
||||
# for LSP mode, start the language server manager
|
||||
if self.get_language_backend().is_lsp():
|
||||
with LogTime("Language server initialization", logger=log):
|
||||
self.reset_language_server_manager()
|
||||
|
||||
# for JetBrains mode, search for plugin server and spawn IDE (if not found and launch command provided)
|
||||
elif self.get_language_backend().is_jetbrains():
|
||||
client = jetbrains_launch_coordinator.find_plugin_server(project)
|
||||
if client is not None:
|
||||
log.info("Found Serena JetBrains Plugin server: %s", client)
|
||||
else:
|
||||
log.info("Serena JetBrains Plugin server not found for project %s", project.project_name)
|
||||
if self.serena_config.jetbrains_launch_command:
|
||||
jetbrains_launch_coordinator.launch_and_wait_for_plugin_server(project, self.serena_config.jetbrains_launch_command)
|
||||
|
||||
def activate_project_from_path_or_name(
|
||||
self, project_root_or_name: str, update_active_modes: bool = True, update_active_tools: bool = True
|
||||
) -> bool:
|
||||
@@ -1604,12 +1577,12 @@ class SerenaAgent:
|
||||
else:
|
||||
result_str += "No active project\n"
|
||||
result_str += f"Agent interface: {self._agent_interface.value}\n"
|
||||
result_str += f"Language backend: {self._language_backend.value}"
|
||||
result_str += f"Language backend: {self._language_backend.get_key()}"
|
||||
if self._active_project and self._active_project.project_config.language_backend is not None:
|
||||
result_str += " (project override)"
|
||||
result_str += f" (global default: {self.serena_config.language_backend.value})\n"
|
||||
if self._language_backend.is_lsp() and self._active_project:
|
||||
result_str += f"Language server status: {self._active_project.get_language_server_manager_status()}\n"
|
||||
result_str += f" (global default: {self.serena_config.language_backend.get_key()})\n"
|
||||
if self._active_project:
|
||||
result_str += self._language_backend.get_config_overview_statement(self._active_project)
|
||||
result_str += "Available projects:\n" + "\n".join(list(self.serena_config.project_names)) + "\n"
|
||||
result_str += f"Active context: {self._context.name}\n"
|
||||
|
||||
|
||||
+7
-7
@@ -24,7 +24,6 @@ from serena.config.client_setup import client_setup_handlers
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.config.serena_config import (
|
||||
AgentInterface,
|
||||
LanguageBackend,
|
||||
ModeSelectionDefinition,
|
||||
ModeSelectionDefinitionWithAddedModes,
|
||||
ProjectConfig,
|
||||
@@ -39,6 +38,7 @@ from serena.constants import (
|
||||
SERENAS_OWN_CONTEXT_YAMLS_DIR,
|
||||
SERENAS_OWN_MODE_YAMLS_DIR,
|
||||
)
|
||||
from serena.language_backend import BuiltinLanguageBackend, LanguageBackendRegistry
|
||||
from serena.prompt_factory import SerenaPromptFactory
|
||||
from serena.tools import ActivateProjectTool
|
||||
from serena.util.cli_util import AutoRegisteringGroup
|
||||
@@ -181,14 +181,14 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
@click.option(
|
||||
"--language-backend",
|
||||
"-b",
|
||||
type=click.Choice([b.value for b in LanguageBackend]),
|
||||
default=LanguageBackend.LSP.value,
|
||||
type=click.Choice([b.value for b in BuiltinLanguageBackend]),
|
||||
default=BuiltinLanguageBackend.LSP.value,
|
||||
show_default=True,
|
||||
help="Default code intelligence backend (can be overridden in the project config).",
|
||||
)
|
||||
def init(language_backend: Literal["LSP", "JetBrains"] = "LSP") -> None:
|
||||
click.echo(f"\nSerena version: {serena_version()}\n")
|
||||
serena_config = SerenaConfig.init(language_backend=LanguageBackend(language_backend))
|
||||
serena_config = SerenaConfig.init(builtin_language_backend=BuiltinLanguageBackend(language_backend))
|
||||
click.echo(f"Configuration file: {serena_config.config_file_path}")
|
||||
click.echo(f"Language backend: {language_backend}")
|
||||
|
||||
@@ -260,7 +260,7 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
)
|
||||
@click.option(
|
||||
"--language-backend",
|
||||
type=click.Choice([lb.value for lb in LanguageBackend]),
|
||||
type=click.Choice(LanguageBackendRegistry.get_instance().get_keys()),
|
||||
default=None,
|
||||
help="Override the configured language backend.",
|
||||
)
|
||||
@@ -393,7 +393,7 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
host=host,
|
||||
port=port,
|
||||
mode_selection_def=mode_selection_def,
|
||||
language_backend=LanguageBackend.from_str(language_backend) if language_backend else None,
|
||||
language_backend=LanguageBackendRegistry.get_instance().resolve(language_backend) if language_backend else None,
|
||||
agent_interface=AgentInterface.from_str(agent_interface) if agent_interface else None,
|
||||
enable_web_dashboard=enable_web_dashboard,
|
||||
open_web_dashboard=open_web_dashboard,
|
||||
@@ -968,7 +968,7 @@ class ProjectCommands(AutoRegisteringGroup):
|
||||
logging.configure(level=logging.INFO)
|
||||
project_path = os.path.abspath(project)
|
||||
serena_config = SerenaConfig.from_config_file().with_headless_mode_overrides()
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
serena_config.set_builtin_language_backend(BuiltinLanguageBackend.LSP)
|
||||
proj = Project.load(project_path, serena_config=serena_config)
|
||||
|
||||
# Create log file with timestamp
|
||||
|
||||
@@ -25,6 +25,7 @@ TSymbol = TypeVar("TSymbol", bound=Symbol)
|
||||
|
||||
class CodeEditor(Generic[TSymbol], ABC):
|
||||
def __init__(self, project: Project) -> None:
|
||||
self.project = project
|
||||
self.project_root = project.project_root
|
||||
self.encoding = project.project_config.encoding
|
||||
self.newline = project.line_ending.newline_str
|
||||
@@ -83,7 +84,7 @@ class CodeEditor(Generic[TSymbol], ABC):
|
||||
"""
|
||||
Context manager for editing a file.
|
||||
"""
|
||||
if FileProxy.is_external_path(relative_path):
|
||||
if FileProxy.is_external_path(relative_path, self.project):
|
||||
raise ValueError(f"Cannot edit external file: {relative_path}")
|
||||
with self._open_file_context(relative_path) as edited_file:
|
||||
yield edited_file
|
||||
|
||||
@@ -41,15 +41,13 @@ from serena.util.yaml import YamlCommentNormalisation, load_yaml, normalise_yaml
|
||||
from solidlsp.ls_config import LanguageServerId, LanguageServerIdLike, LanguageServerRegistry
|
||||
|
||||
from ..analytics import RegisteredTokenCountEstimator
|
||||
from ..language_backend import BuiltinLanguageBackend, LanguageBackend, LanguageBackendRegistry
|
||||
from ..util.class_decorators import singleton
|
||||
from ..util.cli_util import ask_yes_no
|
||||
from ..util.dataclass import get_dataclass_default
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..agent import SerenaAgent
|
||||
from ..project import Project
|
||||
from ..repl.facade import ApiScope, Facade
|
||||
from ..tools.tools_base import Tool
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
T = TypeVar("T")
|
||||
@@ -251,67 +249,6 @@ class AgentInterface(Enum):
|
||||
return self == AgentInterface.REPL
|
||||
|
||||
|
||||
class LanguageBackend(Enum):
|
||||
LSP = "LSP"
|
||||
"""
|
||||
Use the language server protocol (LSP), spawning freely available language servers
|
||||
via the SolidLSP library that is part of Serena
|
||||
"""
|
||||
JETBRAINS = "JetBrains"
|
||||
"""
|
||||
Use the Serena plugin in your JetBrains IDE.
|
||||
(requires the plugin to be installed and the project being worked on to be open in your IDE)
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_str(backend_str: str) -> "LanguageBackend":
|
||||
for backend in LanguageBackend:
|
||||
if backend.value.lower() == backend_str.lower():
|
||||
return backend
|
||||
raise ValueError(f"Unknown language backend '{backend_str}': valid values are {[b.value for b in LanguageBackend]}")
|
||||
|
||||
def is_lsp(self) -> bool:
|
||||
return self == LanguageBackend.LSP
|
||||
|
||||
def is_jetbrains(self) -> bool:
|
||||
return self == LanguageBackend.JETBRAINS
|
||||
|
||||
def get_lsp_tool_class_replacements(self) -> "dict[type[Tool], type[Tool]]":
|
||||
"""
|
||||
:return: mapping from LSP tool classes to replacement tool classes (functional replacements)
|
||||
"""
|
||||
match self:
|
||||
case LanguageBackend.LSP:
|
||||
return {}
|
||||
case LanguageBackend.JETBRAINS:
|
||||
from ..tools import jetbrains_tools, symbol_tools
|
||||
|
||||
return {
|
||||
symbol_tools.FindSymbolTool: jetbrains_tools.JetBrainsFindSymbolTool,
|
||||
symbol_tools.GetSymbolsOverviewTool: jetbrains_tools.JetBrainsGetSymbolsOverviewTool,
|
||||
symbol_tools.FindReferencingSymbolsTool: jetbrains_tools.JetBrainsFindReferencingSymbolsTool,
|
||||
symbol_tools.FindImplementationsTool: jetbrains_tools.JetBrainsFindImplementationsTool,
|
||||
symbol_tools.FindDeclarationTool: jetbrains_tools.JetBrainsFindDeclarationTool,
|
||||
symbol_tools.RenameSymbolTool: jetbrains_tools.JetBrainsRenameTool,
|
||||
symbol_tools.SafeDeleteSymbol: jetbrains_tools.JetBrainsSafeDeleteTool,
|
||||
}
|
||||
case _:
|
||||
raise NotImplementedError()
|
||||
|
||||
def create_facades(self, agent: "SerenaAgent", api_scope: "ApiScope") -> list["Facade"]:
|
||||
from ..repl.facade import Facade
|
||||
|
||||
if self.is_lsp():
|
||||
from ..repl.api.lsp_api import LspApi
|
||||
|
||||
return [Facade.from_api(LspApi(agent), api_scope)]
|
||||
elif self.is_jetbrains():
|
||||
from ..repl.api.jb_api import JetBrainsApi
|
||||
|
||||
return [Facade.from_api(JetBrainsApi(agent), api_scope)]
|
||||
return []
|
||||
|
||||
|
||||
class LineEnding(Enum):
|
||||
"""Line ending convention for file writes."""
|
||||
|
||||
@@ -680,7 +617,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
raise ValueError(f"symbol_info_budget cannot be negative, got: {symbol_info_budget}")
|
||||
|
||||
language_backend_value = data.get("language_backend")
|
||||
language_backend = LanguageBackend.from_str(language_backend_value) if language_backend_value else None
|
||||
language_backend = LanguageBackendRegistry.get_instance().resolve(language_backend_value) if language_backend_value else None
|
||||
agent_interface_value = data.get("agent_interface")
|
||||
agent_interface = AgentInterface.from_str(agent_interface_value) if agent_interface_value else None
|
||||
|
||||
@@ -742,7 +679,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
|
||||
# map fields using non-primitive types to a YAML-compatible representation
|
||||
d["language_servers"] = [lang.get_key() for lang in self.language_servers]
|
||||
d["language_backend"] = self.language_backend.value if self.language_backend is not None else None
|
||||
d["language_backend"] = self.language_backend.get_key() if self.language_backend is not None else None
|
||||
d["agent_interface"] = self.agent_interface.value if self.agent_interface is not None else None
|
||||
d["line_ending"] = self.line_ending.value if self.line_ending is not None else None
|
||||
|
||||
@@ -1020,7 +957,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
Defaults to TOOLS for backward compatibility (as users without this settings will get this default).
|
||||
The default for new users is defined in the template file.
|
||||
"""
|
||||
language_backend: LanguageBackend = LanguageBackend.LSP
|
||||
language_backend: LanguageBackend = field(default_factory=lambda: BuiltinLanguageBackend.LSP.get_instance())
|
||||
"""
|
||||
the language backend to use for code understanding features
|
||||
"""
|
||||
@@ -1204,13 +1141,13 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
language_backend = get_dataclass_default(SerenaConfig, "language_backend")
|
||||
if "language_backend" in loaded_commented_yaml:
|
||||
backend_str = loaded_commented_yaml["language_backend"]
|
||||
language_backend = LanguageBackend.from_str(backend_str)
|
||||
language_backend = LanguageBackendRegistry.get_instance().resolve(backend_str)
|
||||
else:
|
||||
# backward compatibility (migrate Boolean field "jetbrains")
|
||||
if "jetbrains" in loaded_commented_yaml:
|
||||
num_migrations += 1
|
||||
if loaded_commented_yaml["jetbrains"]:
|
||||
language_backend = LanguageBackend.JETBRAINS
|
||||
language_backend = BuiltinLanguageBackend.JETBRAINS.get_instance()
|
||||
del loaded_commented_yaml["jetbrains"]
|
||||
instance.language_backend = language_backend
|
||||
|
||||
@@ -1277,17 +1214,25 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
log.error(f"Error migrating configuration file: {e}")
|
||||
return None
|
||||
|
||||
def set_builtin_language_backend(self, backend: BuiltinLanguageBackend) -> None:
|
||||
"""
|
||||
Sets the built-in language backend to use for code understanding features.
|
||||
|
||||
:param backend: the language backend to set
|
||||
"""
|
||||
self.language_backend = backend.get_instance()
|
||||
|
||||
@classmethod
|
||||
def init(cls, language_backend: LanguageBackend) -> "SerenaConfig":
|
||||
def init(cls, builtin_language_backend: BuiltinLanguageBackend) -> "SerenaConfig":
|
||||
"""
|
||||
Supports the config initialisation CLI command, allowing the user to configure fundamental settings before
|
||||
the first launch.
|
||||
|
||||
:param language_backend: the language backend to use
|
||||
:param builtin_language_backend: the language backend to use
|
||||
:return: the created SerenaConfig instance
|
||||
"""
|
||||
config = cls.from_config_file()
|
||||
config.language_backend = language_backend
|
||||
config.language_backend = builtin_language_backend.get_instance()
|
||||
config._save()
|
||||
return config
|
||||
|
||||
@@ -1479,7 +1424,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
commented_yaml["projects"] = sorted({str(project.project_root) for project in self.projects})
|
||||
|
||||
# convert language backend to string
|
||||
commented_yaml["language_backend"] = self.language_backend.value
|
||||
commented_yaml["language_backend"] = self.language_backend.get_key()
|
||||
|
||||
# convert agent interface to string (None if not configured)
|
||||
commented_yaml["agent_interface"] = self.agent_interface.value if self.agent_interface is not None else None
|
||||
@@ -1609,7 +1554,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
log.info(f"Using agent interface '{agent_interface.value}' ({source})")
|
||||
return agent_interface
|
||||
|
||||
def determine_language_backend(self, project_config: ProjectConfig | None = None, log_choice: bool = False):
|
||||
def determine_language_backend(self, project_config: ProjectConfig | None = None, log_choice: bool = False) -> LanguageBackend:
|
||||
language_backend = self.language_backend
|
||||
if project_config and project_config.language_backend is not None:
|
||||
language_backend = project_config.language_backend
|
||||
|
||||
@@ -651,7 +651,7 @@ class SerenaDashboardAPI:
|
||||
modes=modes_info,
|
||||
active_tools=active_tools,
|
||||
agent_interface=self._agent.get_agent_interface().value,
|
||||
language_backend=self._agent.get_language_backend().value,
|
||||
language_backend=self._agent.get_language_backend().get_key(),
|
||||
facades=facades,
|
||||
tool_stats_summary=tool_stats_summary,
|
||||
registered_projects=registered_projects,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from overrides import override
|
||||
|
||||
from serena.code_editor import JetBrainsCodeEditor
|
||||
from serena.jetbrains import launch_coordinator
|
||||
from serena.language_backend import BuiltinLanguageBackend, LanguageBackend
|
||||
|
||||
from ..util.file_proxy import FileProxy, LocalProjectFileProxy
|
||||
from . import jetbrains_types as jb
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.code_editor import CodeEditor
|
||||
from serena.project import Project
|
||||
from serena.repl.facade import ApiScope, Facade
|
||||
from serena.tools import Tool
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LanguageBackendJetBrains(LanguageBackend):
|
||||
def __init__(self):
|
||||
super().__init__(BuiltinLanguageBackend.JETBRAINS.value)
|
||||
|
||||
@override
|
||||
def get_lsp_tool_class_replacements(self) -> "dict[type[Tool], type[Tool]]":
|
||||
from ..tools import jetbrains_tools, symbol_tools
|
||||
|
||||
return {
|
||||
symbol_tools.FindSymbolTool: jetbrains_tools.JetBrainsFindSymbolTool,
|
||||
symbol_tools.GetSymbolsOverviewTool: jetbrains_tools.JetBrainsGetSymbolsOverviewTool,
|
||||
symbol_tools.FindReferencingSymbolsTool: jetbrains_tools.JetBrainsFindReferencingSymbolsTool,
|
||||
symbol_tools.FindImplementationsTool: jetbrains_tools.JetBrainsFindImplementationsTool,
|
||||
symbol_tools.FindDeclarationTool: jetbrains_tools.JetBrainsFindDeclarationTool,
|
||||
symbol_tools.RenameSymbolTool: jetbrains_tools.JetBrainsRenameTool,
|
||||
symbol_tools.SafeDeleteSymbol: jetbrains_tools.JetBrainsSafeDeleteTool,
|
||||
}
|
||||
|
||||
@override
|
||||
def create_facades(self, agent: "SerenaAgent", api_scope: "ApiScope") -> list["Facade"]:
|
||||
from ..repl.api.jb_api import JetBrainsApi
|
||||
from ..repl.facade import Facade
|
||||
|
||||
return [Facade.from_api(JetBrainsApi(agent), api_scope)]
|
||||
|
||||
@override
|
||||
def init_active_project(self, agent: "SerenaAgent") -> None:
|
||||
project = agent.get_active_project_or_raise()
|
||||
client = launch_coordinator.find_plugin_server(project)
|
||||
if client is not None:
|
||||
log.info("Found Serena JetBrains Plugin server: %s", client)
|
||||
else:
|
||||
log.info("Serena JetBrains Plugin server not found for project %s", project.project_name)
|
||||
launch_command = agent.serena_config.jetbrains_launch_command
|
||||
if launch_command:
|
||||
launch_coordinator.launch_and_wait_for_plugin_server(project, launch_command)
|
||||
|
||||
@override
|
||||
def shutdown_active_project(self, project: "Project", timeout: float) -> None:
|
||||
pass
|
||||
|
||||
@override
|
||||
def create_code_editor(self, project: "Project") -> "CodeEditor":
|
||||
return JetBrainsCodeEditor(project)
|
||||
|
||||
@override
|
||||
def is_source_file(self, abs_path: str, project: "Project") -> bool:
|
||||
# no distinction is made; every file is potentially a source file
|
||||
return True
|
||||
|
||||
@override
|
||||
def is_external_path(self, relative_path: str) -> bool:
|
||||
return relative_path.startswith(jb.JB_EXTERNAL_FILE_PREFIX)
|
||||
|
||||
@override
|
||||
def create_file_proxy(self, relative_path: str, project: "Project") -> FileProxy:
|
||||
if self.is_external_path(relative_path):
|
||||
return JetBrainsFileProxy(relative_path, project)
|
||||
return LocalProjectFileProxy(relative_path, project)
|
||||
|
||||
|
||||
class JetBrainsFileProxy(FileProxy):
|
||||
"""
|
||||
Retrieves the contents of a file from the JetBrains plugin via the plugin client, given its relative path,
|
||||
which may be an external path (e.g., "<ext:FileUtil.class|472e0a13>")
|
||||
"""
|
||||
|
||||
def __init__(self, relative_path: str, project: "Project"):
|
||||
self._relative_path = relative_path
|
||||
self._project = project
|
||||
|
||||
def get_contents(self) -> str:
|
||||
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient
|
||||
|
||||
client = JetBrainsPluginClient.from_project(self._project)
|
||||
return client.read_file(self._relative_path)
|
||||
|
||||
def get_relative_path(self) -> str:
|
||||
return self._relative_path
|
||||
|
||||
def is_glob_supported(self):
|
||||
return False
|
||||
@@ -8,14 +8,6 @@ Prefix used for in relative paths of symbols that are from external libraries (i
|
||||
"""
|
||||
|
||||
|
||||
def is_external_path(relative_path: str):
|
||||
"""
|
||||
:param relative_path: a relative path (e.g., from a symbol's `relative_path` field)
|
||||
:return: whether the path is an external path (i.e., from a library, not the user's codebase)
|
||||
"""
|
||||
return relative_path.startswith(JB_EXTERNAL_FILE_PREFIX)
|
||||
|
||||
|
||||
class PluginStatusDTO(TypedDict):
|
||||
project_root: str
|
||||
plugin_version: str
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import importlib
|
||||
import logging
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from functools import cache
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from serena.util.file_proxy import FileProxy
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.code_editor import CodeEditor
|
||||
from serena.project import Project
|
||||
from serena.repl.facade import ApiScope, Facade
|
||||
from serena.tools import Tool
|
||||
|
||||
|
||||
class LanguageBackend(ABC):
|
||||
def __init__(self, key: str):
|
||||
"""
|
||||
:param key: the key by which the backend is identified in the registry and in configuration
|
||||
"""
|
||||
self._key = key
|
||||
|
||||
def __str__(self):
|
||||
return self._key
|
||||
|
||||
def get_key(self) -> str:
|
||||
"""
|
||||
:return: the key by which the backend is identified in the registry and in configuration
|
||||
"""
|
||||
return self._key
|
||||
|
||||
def is_lsp(self):
|
||||
return self.get_key() == BuiltinLanguageBackend.LSP.value
|
||||
|
||||
def is_jetbrains(self):
|
||||
return self.get_key() == BuiltinLanguageBackend.JETBRAINS.value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.get_key()
|
||||
|
||||
@abstractmethod
|
||||
def get_lsp_tool_class_replacements(self) -> "dict[type[Tool], type[Tool]]":
|
||||
"""
|
||||
:return: mapping from LSP tool classes to replacement tool classes (functional replacements)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def create_facades(self, agent: "SerenaAgent", api_scope: "ApiScope") -> list["Facade"]:
|
||||
"""
|
||||
Creates backend-specific facades for the given agent and API scope.
|
||||
|
||||
:param agent: the agent
|
||||
:param api_scope: the API scope defining active facade methods
|
||||
:return: the list of facades to be used by the agent for this backend
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def init_active_project(self, agent: "SerenaAgent") -> None:
|
||||
"""
|
||||
Initialises the backend for the given agent's newly activated project.
|
||||
|
||||
:param agent: the agent, which has just set a new active project
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def shutdown_active_project(self, project: "Project", timeout: float) -> None:
|
||||
"""
|
||||
Cleans up, freeing resources, after a project has been deactivated.
|
||||
|
||||
:param project: the project
|
||||
:param timeout: the timeout, in seconds, after which to give up on graceful shutdown
|
||||
"""
|
||||
|
||||
def get_project_activation_statement(self, project: "Project") -> str:
|
||||
"""
|
||||
:return: a statement to add to the project activation message
|
||||
"""
|
||||
return ""
|
||||
|
||||
def get_config_overview_statement(self, project: "Project") -> str:
|
||||
"""
|
||||
:return: a statement to add to the project configuration overview
|
||||
"""
|
||||
return ""
|
||||
|
||||
@abstractmethod
|
||||
def create_code_editor(self, project: "Project") -> "CodeEditor":
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_source_file(self, abs_path: str, project: "Project") -> bool:
|
||||
"""
|
||||
Determines whether the given absolute path corresponds to a source file that can (potentially) be processed/understood by the backend.
|
||||
|
||||
:param abs_path: the absolute path to an existing file
|
||||
:param project: the project in which the file is located
|
||||
:return: True if the file is a source file for this backend (or the backend does not specifically make distinctions),
|
||||
False otherwise
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def is_external_path(self, relative_path: str) -> bool:
|
||||
"""
|
||||
Determines whether the given relative path corresponds to a file that is external to the project (e.g. a dependency file).
|
||||
Virtually all of Serena's interfaces use `relative_path` (relative to the project root) to refer to files, but some backends
|
||||
may need to support project-external files. In this case, the external path should be encoded in the `relative_path` parameter
|
||||
(e.g. "<ext:/path/to/whatever>") rather than this being an actual relative path that points outside the project root.
|
||||
Therefore, information about the project in question is deliberately not provided to this method.
|
||||
|
||||
:param relative_path: the relative path to a file within the project or an encoded external path.
|
||||
The path can be assumed to have been provided by the backend itself.
|
||||
:return: whether the file is considered external to the project by this backend
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def create_file_proxy(self, relative_path: str, project: "Project") -> FileProxy:
|
||||
"""
|
||||
Creates a file proxy for the given relative path in the given project.
|
||||
|
||||
:param relative_path: the relative path to a file within the project or an encoded external path.
|
||||
:param project: the project
|
||||
:return: a file proxy for the given file
|
||||
"""
|
||||
|
||||
|
||||
class BuiltinLanguageBackend(Enum):
|
||||
LSP = "LSP"
|
||||
"""
|
||||
Use the language server protocol (LSP), spawning freely available language servers
|
||||
via the SolidLSP library that is part of Serena
|
||||
"""
|
||||
JETBRAINS = "JetBrains"
|
||||
"""
|
||||
Use the Serena plugin in your JetBrains IDE.
|
||||
(requires the plugin to be installed and the project being worked on to be open in your IDE)
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_str(backend_str: str) -> "BuiltinLanguageBackend":
|
||||
for backend in BuiltinLanguageBackend:
|
||||
if backend.value.lower() == backend_str.lower():
|
||||
return backend
|
||||
raise ValueError(f"Unknown language backend '{backend_str}': valid values are {[b.value for b in BuiltinLanguageBackend]}")
|
||||
|
||||
@cache
|
||||
def get_instance(self) -> LanguageBackend:
|
||||
if self == BuiltinLanguageBackend.LSP:
|
||||
from .lsp.lsp_backend import LanguageBackendLSP
|
||||
|
||||
return LanguageBackendLSP()
|
||||
elif self == BuiltinLanguageBackend.JETBRAINS:
|
||||
from .jetbrains.jetbrains_backend import LanguageBackendJetBrains
|
||||
|
||||
return LanguageBackendJetBrains()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class LanguageBackendRegistry:
|
||||
"""
|
||||
Registry of language backends
|
||||
"""
|
||||
|
||||
REGISTRATION_ENTRY_POINT_GROUP = "serena.language_backend_registration"
|
||||
"""
|
||||
entry point group for language backend registration functions; each function should call use
|
||||
`LanguageBackendRegistry.get_instance().register(...)` to register a backend
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
_instance_lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
with cls._instance_lock:
|
||||
if cls._instance is None:
|
||||
cls._instance = cls(True)
|
||||
cls._discover_backends_from_entry_points()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, _singleton: bool):
|
||||
if not _singleton:
|
||||
raise RuntimeError("LanguageServerRegistry is a singleton. Use get_instance() to access it.")
|
||||
self._registered_backends: dict[str, LanguageBackend] = {}
|
||||
|
||||
# auto-register built-in language backends
|
||||
for builtin_backend in BuiltinLanguageBackend:
|
||||
self._registered_backends[builtin_backend.value] = builtin_backend.get_instance()
|
||||
|
||||
@classmethod
|
||||
def _discover_backends_from_entry_points(cls) -> None:
|
||||
"""
|
||||
Discover and execute language server adapter registration functions from entry points.
|
||||
"""
|
||||
log.debug("Discovering language backend registration entry points ...")
|
||||
try:
|
||||
entry_points = importlib.metadata.entry_points(group=cls.REGISTRATION_ENTRY_POINT_GROUP)
|
||||
except Exception as error:
|
||||
log.exception("Failed to discover language server registration entry points: %s", error)
|
||||
return
|
||||
|
||||
def get_distribution_name(ep: importlib.metadata.EntryPoint) -> str:
|
||||
distribution = getattr(ep, "dist", None)
|
||||
if distribution is None:
|
||||
return "unknown distribution"
|
||||
return distribution.name or "unknown distribution"
|
||||
|
||||
log.debug("Found %d language server registration entry points", len(entry_points))
|
||||
for entry_point in entry_points:
|
||||
try:
|
||||
registration = entry_point.load()
|
||||
if not callable(registration):
|
||||
raise TypeError("Entry point must resolve to a callable registration function")
|
||||
registration()
|
||||
except Exception as error:
|
||||
log.exception(
|
||||
"Failed to load language backend entry point '%s' from %s: %s",
|
||||
entry_point.name,
|
||||
get_distribution_name(entry_point),
|
||||
error,
|
||||
)
|
||||
|
||||
def resolve(self, key: str) -> LanguageBackend:
|
||||
if key in self._registered_backends:
|
||||
return self._registered_backends[key]
|
||||
raise ValueError(f"Unknown language backend key: '{key}'; Valid keys: {self.get_keys()}")
|
||||
|
||||
def register(self, backend: LanguageBackend, allow_override: bool = False) -> None:
|
||||
"""
|
||||
:param backend: the backend to register
|
||||
:param allow_override: whether to allow overriding an existing registration with the same key
|
||||
"""
|
||||
key = backend.get_key()
|
||||
log.info("Registering language backend: %s (class=%s)", key, backend.__class__.__name__)
|
||||
if backend.get_key() in self._registered_backends and not allow_override:
|
||||
raise ValueError(f"Language backend already registered: {key}")
|
||||
self._registered_backends[key] = backend
|
||||
|
||||
def get_keys(self) -> list[str]:
|
||||
"""
|
||||
:return: the sorted list of all registered string keys
|
||||
"""
|
||||
return sorted(self._registered_backends.keys())
|
||||
@@ -0,0 +1,80 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from overrides import override
|
||||
from sensai.util.logging import LogTime
|
||||
|
||||
from serena.language_backend import BuiltinLanguageBackend, LanguageBackend
|
||||
from serena.util.file_proxy import FileProxy, LocalProjectFileProxy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.code_editor import LanguageServerCodeEditor
|
||||
from serena.project import Project
|
||||
from serena.repl.facade import ApiScope, Facade
|
||||
from serena.tools import Tool
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LanguageBackendLSP(LanguageBackend):
|
||||
def __init__(self):
|
||||
super().__init__(BuiltinLanguageBackend.LSP.value)
|
||||
|
||||
@override
|
||||
def get_lsp_tool_class_replacements(self) -> "dict[type[Tool], type[Tool]]":
|
||||
return {}
|
||||
|
||||
@override
|
||||
def create_facades(self, agent: "SerenaAgent", api_scope: "ApiScope") -> list["Facade"]:
|
||||
from ..repl.api.lsp_api import LspApi
|
||||
from ..repl.facade import Facade
|
||||
|
||||
return [Facade.from_api(LspApi(agent), api_scope)]
|
||||
|
||||
@override
|
||||
def init_active_project(self, agent: "SerenaAgent") -> None:
|
||||
with LogTime("Language server initialization", logger=log):
|
||||
agent.reset_language_server_manager()
|
||||
|
||||
@override
|
||||
def shutdown_active_project(self, project: "Project", timeout: float) -> None:
|
||||
# nothing to do; the language server manager is already shut down by the project itself
|
||||
pass
|
||||
|
||||
@override
|
||||
def get_project_activation_statement(self, project: "Project") -> str:
|
||||
language_servers_str = ", ".join([ls.get_key() for ls in project.project_config.language_servers])
|
||||
return f"Active language servers: {language_servers_str}.\n"
|
||||
|
||||
@override
|
||||
def get_config_overview_statement(self, project: "Project") -> str:
|
||||
return f"Language server status: {project.get_language_server_manager_status()}\n"
|
||||
|
||||
@override
|
||||
def create_code_editor(self, project: "Project") -> "LanguageServerCodeEditor":
|
||||
from serena.code_editor import LanguageServerCodeEditor
|
||||
from serena.symbol import LanguageServerSymbolRetriever
|
||||
|
||||
symbol_retriever = LanguageServerSymbolRetriever(project)
|
||||
return LanguageServerCodeEditor(symbol_retriever)
|
||||
|
||||
@override
|
||||
def is_source_file(self, abs_path: str, project: "Project") -> bool:
|
||||
is_file_in_supported_languages = False
|
||||
for language in project.project_config.language_servers:
|
||||
fn_matcher = language.get_source_fn_matcher()
|
||||
if fn_matcher.is_relevant_filename(abs_path):
|
||||
is_file_in_supported_languages = True
|
||||
break
|
||||
return is_file_in_supported_languages
|
||||
|
||||
@override
|
||||
def is_external_path(self, relative_path: str) -> bool:
|
||||
# LSP backend currently uses only true project-relative paths
|
||||
return False
|
||||
|
||||
@override
|
||||
def create_file_proxy(self, relative_path: str, project: "Project") -> FileProxy:
|
||||
return LocalProjectFileProxy(relative_path, project)
|
||||
@@ -219,7 +219,7 @@ class DiagnosticsContext:
|
||||
DIAGNOSTICS_KEY = "diagnostics[warning-or-higher]"
|
||||
|
||||
def __init__(self, agent: "SerenaAgent", *edited_relative_paths: str, enable: bool = ENABLE_DIAGNOSTICS_DEFAULT) -> None:
|
||||
self._is_diagnostics_enabled = enable and agent.is_using_language_server()
|
||||
self._is_diagnostics_enabled = enable and agent.get_language_backend()
|
||||
self._edited_files = [EditedFilePath(path, path) for path in edited_relative_paths]
|
||||
self._before_edit_diagnostics_snapshot: PublishedDiagnosticsSnapshot | None = None
|
||||
self._symbol_retriever: Optional["LanguageServerSymbolRetriever"] | None = None
|
||||
|
||||
+2
-1
@@ -26,8 +26,9 @@ from serena.agent import (
|
||||
SerenaAgent,
|
||||
)
|
||||
from serena.config.context_mode import SerenaAgentContext
|
||||
from serena.config.serena_config import AgentInterface, LanguageBackend, ModeSelectionDefinition, SerenaConfig
|
||||
from serena.config.serena_config import AgentInterface, ModeSelectionDefinition, SerenaConfig
|
||||
from serena.constants import DEFAULT_CONTEXT, SERENA_LOG_FORMAT
|
||||
from serena.language_backend import LanguageBackend
|
||||
from serena.tools import Tool, ToolCallError
|
||||
from serena.util.exception import show_fatal_exception_safe
|
||||
from serena.util.logging import MemoryLogHandler
|
||||
|
||||
+16
-16
@@ -12,11 +12,11 @@ from sensai.util.logging import LogTime
|
||||
from sensai.util.string import TextBuilder, ToStringMixin
|
||||
|
||||
from serena.config.serena_config import (
|
||||
LanguageBackend,
|
||||
ProjectConfig,
|
||||
ProjectConfigAutoGenerationMode,
|
||||
SerenaConfig,
|
||||
)
|
||||
from serena.language_backend import LanguageBackend
|
||||
from serena.ls_manager import LanguageServerFactory, LanguageServerManager
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
from serena.util.file_proxy import FileCollection, FileProxy
|
||||
@@ -128,7 +128,7 @@ class Project(ToStringMixin):
|
||||
@property
|
||||
def language_backend(self) -> LanguageBackend:
|
||||
# The backend configuration is fundamentally owned by the agent, so it takes
|
||||
# precedence. (Note: The agent does not necessary honour the project's choice,
|
||||
# precedence. (Note: The agent does not necessarily honour the project's choice,
|
||||
# as it may be invalid.)
|
||||
if self._agent is not None:
|
||||
return self._agent.get_language_backend()
|
||||
@@ -236,18 +236,10 @@ class Project(ToStringMixin):
|
||||
|
||||
# check code file restriction (depending on backend)
|
||||
if ignore_non_source_files:
|
||||
# apply restriction only for LSP backend, which enumerates known languages
|
||||
# and therefore can determine whether a file is a source file or not
|
||||
if self.language_backend.is_lsp():
|
||||
if os.path.isfile(abs_path):
|
||||
is_file_in_supported_language = False
|
||||
for language in self.project_config.language_servers:
|
||||
fn_matcher = language.get_source_fn_matcher()
|
||||
if fn_matcher.is_relevant_filename(abs_path):
|
||||
is_file_in_supported_language = True
|
||||
break
|
||||
if not is_file_in_supported_language:
|
||||
return True
|
||||
if os.path.isfile(abs_path):
|
||||
# non-source files are ignored
|
||||
if not self.language_backend.is_source_file(abs_path, self):
|
||||
return True
|
||||
|
||||
# Create normalized path for consistent handling
|
||||
rel_path = Path(relative_path)
|
||||
@@ -336,7 +328,7 @@ class Project(ToStringMixin):
|
||||
:param relative_path: the path to validate, relative to the project root
|
||||
:param require_not_ignored: if True, the path must not be ignored according to the project's ignore settings
|
||||
"""
|
||||
if FileProxy.is_external_path(relative_path):
|
||||
if FileProxy.is_external_path(relative_path, self):
|
||||
return
|
||||
|
||||
if not self.is_path_in_project(relative_path):
|
||||
@@ -392,7 +384,7 @@ class Project(ToStringMixin):
|
||||
:param skip_ignored_files: whether to skip ignored files; has no effect if `code_files_only` is True
|
||||
:return:
|
||||
"""
|
||||
if FileProxy.is_external_path(relative_path):
|
||||
if FileProxy.is_external_path(relative_path, self):
|
||||
# single external path: create appropriate proxy
|
||||
file_collection = FileCollection([FileProxy.from_project_relative_path(self, relative_path)])
|
||||
else:
|
||||
@@ -621,6 +613,14 @@ class Project(ToStringMixin):
|
||||
return 0
|
||||
|
||||
def shutdown(self, timeout: float = 2.0) -> None:
|
||||
"""
|
||||
Shuts down the project, calling the language backend-specific shutdown of the active project.
|
||||
|
||||
:param timeout: the timeout, in seconds
|
||||
"""
|
||||
# clean up internal resources
|
||||
if self.language_server_manager is not None:
|
||||
self.language_server_manager.stop_all(save_cache=True, timeout=timeout)
|
||||
self.language_server_manager = None
|
||||
# trigger additional backend-specific shutdown
|
||||
self.language_backend.shutdown_active_project(self, timeout=timeout)
|
||||
@@ -12,8 +12,9 @@ from flask import Flask, Response, abort, request
|
||||
from pydantic import BaseModel
|
||||
from sensai.util.logging import LogTime
|
||||
|
||||
from serena.config.serena_config import LanguageBackend, SerenaConfig
|
||||
from serena.config.serena_config import SerenaConfig
|
||||
from serena.constants import SerenaPorts
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.project import Project
|
||||
@@ -73,7 +74,7 @@ class ProjectServer:
|
||||
port = self.PORT
|
||||
|
||||
serena_config = SerenaConfig.from_config_file().with_headless_mode_overrides()
|
||||
serena_config.language_backend = LanguageBackend.LSP
|
||||
serena_config.set_builtin_language_backend(BuiltinLanguageBackend.LSP)
|
||||
|
||||
self._agent = SerenaAgent(serena_config=serena_config)
|
||||
self._loaded_projects_by_root: dict[str, "Project"] = {}
|
||||
|
||||
@@ -362,17 +362,9 @@ class FacadeApi(ABC):
|
||||
"""
|
||||
:return: a code editor for the active project, using the active language backend
|
||||
"""
|
||||
from serena.code_editor import JetBrainsCodeEditor, LanguageServerCodeEditor
|
||||
from serena.symbol import LanguageServerSymbolRetriever
|
||||
|
||||
project = self._get_project()
|
||||
backend = self._agent.get_language_backend()
|
||||
if backend.is_lsp():
|
||||
return LanguageServerCodeEditor(LanguageServerSymbolRetriever(project))
|
||||
elif backend.is_jetbrains():
|
||||
return JetBrainsCodeEditor(project)
|
||||
else:
|
||||
raise ValueError(f"Unsupported language backend: {backend}")
|
||||
return backend.create_code_editor(project)
|
||||
|
||||
|
||||
class FacadeMethod:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import json
|
||||
|
||||
from serena.config.serena_config import LanguageBackend
|
||||
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClientManager
|
||||
from serena.project_server import ProjectServerClient
|
||||
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
|
||||
@@ -67,13 +66,10 @@ class QueryProjectTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveP
|
||||
return tool.apply(**json.loads(tool_params_json))
|
||||
|
||||
def _is_project_server_required(self, tool: Tool) -> bool:
|
||||
match self.agent.get_language_backend():
|
||||
case LanguageBackend.JETBRAINS:
|
||||
return False
|
||||
case LanguageBackend.LSP:
|
||||
# Note: As long as only read-only tools are considered, only symbolic tools require the project server.
|
||||
# But if we were to allow non-read-only tools, then tools using a CodeEditor also indirectly require language servers.
|
||||
assert tool.is_readonly()
|
||||
return tool.is_symbolic()
|
||||
case _:
|
||||
raise NotImplementedError
|
||||
# The project server is relevant to the LSP backend only
|
||||
if not self.agent.get_language_backend().is_lsp():
|
||||
return False
|
||||
# Note: As long as only read-only tools are considered, only symbolic tools require the project server.
|
||||
# But if we were to allow non-read-only tools, then tools using a CodeEditor also indirectly require language servers.
|
||||
assert tool.is_readonly()
|
||||
return tool.is_symbolic()
|
||||
@@ -16,7 +16,6 @@ from sensai.util.helper import mark_used
|
||||
from sensai.util.string import dict_string
|
||||
|
||||
from serena.code_editor import EditedFileContext
|
||||
from serena.config.serena_config import LanguageBackend
|
||||
from serena.lsp.lsp_diagnostics import DiagnosticsContext
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
from serena.project import Project
|
||||
@@ -29,7 +28,7 @@ from solidlsp.ls_exceptions import SolidLSPException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.code_editor import CodeEditor, LanguageServerCodeEditor
|
||||
from serena.code_editor import CodeEditor
|
||||
from serena.symbol import LanguageServerSymbolRetriever
|
||||
|
||||
|
||||
@@ -67,22 +66,7 @@ class Component(ABC):
|
||||
return self.agent.get_active_project_or_raise()
|
||||
|
||||
def create_code_editor(self) -> "CodeEditor":
|
||||
from ..code_editor import JetBrainsCodeEditor
|
||||
|
||||
match self.agent.get_language_backend():
|
||||
case LanguageBackend.LSP:
|
||||
return self.create_ls_code_editor()
|
||||
case LanguageBackend.JETBRAINS:
|
||||
return JetBrainsCodeEditor(project=self.project)
|
||||
case _:
|
||||
raise ValueError
|
||||
|
||||
def create_ls_code_editor(self) -> "LanguageServerCodeEditor":
|
||||
from ..code_editor import LanguageServerCodeEditor
|
||||
|
||||
if not self.agent.is_using_language_server():
|
||||
raise Exception("Cannot create LanguageServerCodeEditor; agent is not in language server mode.")
|
||||
return LanguageServerCodeEditor(self.create_language_server_symbol_retriever())
|
||||
return self.agent.get_language_backend().create_code_editor(self.project)
|
||||
|
||||
|
||||
class ToolMarker:
|
||||
|
||||
@@ -6,8 +6,6 @@ from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator
|
||||
from typing import TYPE_CHECKING, Self
|
||||
|
||||
from serena.jetbrains import jetbrains_types as jb
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.project import Project
|
||||
|
||||
@@ -30,19 +28,15 @@ class FileProxy(ABC):
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_external_path(relative_path: str) -> bool:
|
||||
def is_external_path(relative_path: str, project: "Project") -> bool:
|
||||
"""
|
||||
:return: whether the given relative path is an encoded external path (not a local project file)
|
||||
"""
|
||||
# This is intended to be extended once we also support external paths in other backends
|
||||
return jb.is_external_path(relative_path)
|
||||
return project.language_backend.is_external_path(relative_path)
|
||||
|
||||
@classmethod
|
||||
def from_project_relative_path(cls, project: "Project", relative_path: str) -> "FileProxy":
|
||||
if cls.is_external_path(relative_path):
|
||||
if project.language_backend.is_jetbrains():
|
||||
return JetBrainsFileProxy(relative_path, project)
|
||||
return LocalProjectFileProxy(relative_path, project)
|
||||
return project.language_backend.create_file_proxy(relative_path, project)
|
||||
|
||||
|
||||
class LocalProjectFileProxy(FileProxy):
|
||||
@@ -62,29 +56,6 @@ class LocalProjectFileProxy(FileProxy):
|
||||
return True
|
||||
|
||||
|
||||
class JetBrainsFileProxy(FileProxy):
|
||||
"""
|
||||
Retrieves the contents of a file from the JetBrains plugin via the plugin client, given its relative path,
|
||||
which may be an external path (e.g., "<ext:FileUtil.class|472e0a13>")
|
||||
"""
|
||||
|
||||
def __init__(self, relative_path: str, project: "Project"):
|
||||
self._relative_path = relative_path
|
||||
self._project = project
|
||||
|
||||
def get_contents(self) -> str:
|
||||
from serena.jetbrains.jetbrains_plugin_client import JetBrainsPluginClient
|
||||
|
||||
client = JetBrainsPluginClient.from_project(self._project)
|
||||
return client.read_file(self._relative_path)
|
||||
|
||||
def get_relative_path(self) -> str:
|
||||
return self._relative_path
|
||||
|
||||
def is_glob_supported(self):
|
||||
return False
|
||||
|
||||
|
||||
class FileCollection:
|
||||
def __init__(self, file_proxies: list[FileProxy]):
|
||||
self._file_proxies = file_proxies
|
||||
|
||||
@@ -12,13 +12,13 @@ from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import (
|
||||
DEFAULT_PROJECT_SERENA_FOLDER_LOCATION,
|
||||
AgentInterface,
|
||||
LanguageBackend,
|
||||
ProjectConfig,
|
||||
RegisteredProject,
|
||||
SerenaConfig,
|
||||
SerenaConfigError,
|
||||
)
|
||||
from serena.constants import PROJECT_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
from serena.project import MemoryManager, Project
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from test.conftest import create_default_serena_config
|
||||
@@ -178,15 +178,16 @@ class TestProjectConfigLanguageBackend:
|
||||
config = ProjectConfig(
|
||||
project_name="test",
|
||||
language_servers=[LanguageServerId.PYTHON],
|
||||
language_backend=LanguageBackend.JETBRAINS,
|
||||
language_backend=BuiltinLanguageBackend.JETBRAINS.get_instance(),
|
||||
)
|
||||
assert config.language_backend == LanguageBackend.JETBRAINS
|
||||
assert config.language_backend is not None
|
||||
assert config.language_backend.is_jetbrains()
|
||||
|
||||
def test_language_backend_roundtrips_through_yaml(self):
|
||||
config = ProjectConfig(
|
||||
project_name="test",
|
||||
language_servers=[LanguageServerId.PYTHON],
|
||||
language_backend=LanguageBackend.JETBRAINS,
|
||||
language_backend=BuiltinLanguageBackend.JETBRAINS.get_instance(),
|
||||
)
|
||||
d = config._to_yaml_dict()
|
||||
assert d["language_backend"] == "JetBrains"
|
||||
@@ -207,7 +208,8 @@ class TestProjectConfigLanguageBackend:
|
||||
data["languages"] = ["python"]
|
||||
data["language_backend"] = "JetBrains"
|
||||
config = ProjectConfig._from_dict(data, local_override_keys=[])
|
||||
assert config.language_backend == LanguageBackend.JETBRAINS
|
||||
assert config.language_backend is not None
|
||||
assert config.language_backend.is_jetbrains()
|
||||
|
||||
def test_language_backend_none_when_missing_from_dict(self):
|
||||
"""Test that _from_dict handles missing language_backend gracefully."""
|
||||
@@ -284,20 +286,20 @@ class TestAgentInterface:
|
||||
|
||||
def _make_config_with_project(
|
||||
project_name: str,
|
||||
language_backend: LanguageBackend | None = None,
|
||||
global_backend: LanguageBackend = LanguageBackend.LSP,
|
||||
language_backend: BuiltinLanguageBackend | None = None,
|
||||
global_backend: BuiltinLanguageBackend = BuiltinLanguageBackend.LSP,
|
||||
) -> tuple[SerenaConfig, str]:
|
||||
"""Create a SerenaConfig with a single registered project and return (config, project_name)."""
|
||||
config = SerenaConfig(
|
||||
log_level=logging.ERROR,
|
||||
language_backend=global_backend,
|
||||
language_backend=global_backend.get_instance(),
|
||||
).with_headless_mode_overrides()
|
||||
project = Project(
|
||||
project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "python" / "test_repo"),
|
||||
project_config=ProjectConfig(
|
||||
project_name=project_name,
|
||||
language_servers=[LanguageServerId.PYTHON],
|
||||
language_backend=language_backend,
|
||||
language_backend=language_backend.get_instance() if language_backend is not None else None,
|
||||
),
|
||||
serena_config=config,
|
||||
)
|
||||
@@ -310,7 +312,7 @@ class TestEffectiveLanguageBackend:
|
||||
|
||||
def test_default_backend_is_global(self):
|
||||
"""When no project override, effective backend matches global config."""
|
||||
config, name = _make_config_with_project("test_proj", language_backend=None, global_backend=LanguageBackend.LSP)
|
||||
config, name = _make_config_with_project("test_proj", language_backend=None, global_backend=BuiltinLanguageBackend.LSP)
|
||||
agent = SerenaAgent(project=name, serena_config=config)
|
||||
try:
|
||||
assert agent.get_language_backend().is_lsp()
|
||||
@@ -320,7 +322,7 @@ class TestEffectiveLanguageBackend:
|
||||
def test_project_overrides_global_backend(self):
|
||||
"""When startup project has language_backend set, it overrides the global."""
|
||||
config, name = _make_config_with_project(
|
||||
"test_jetbrains", language_backend=LanguageBackend.JETBRAINS, global_backend=LanguageBackend.LSP
|
||||
"test_jetbrains", language_backend=BuiltinLanguageBackend.JETBRAINS, global_backend=BuiltinLanguageBackend.LSP
|
||||
)
|
||||
agent = SerenaAgent(project=name, serena_config=config)
|
||||
try:
|
||||
@@ -332,18 +334,18 @@ class TestEffectiveLanguageBackend:
|
||||
"""When no startup project is provided, effective backend is the global one."""
|
||||
config = SerenaConfig(
|
||||
log_level=logging.ERROR,
|
||||
language_backend=LanguageBackend.LSP,
|
||||
language_backend=BuiltinLanguageBackend.LSP.get_instance(),
|
||||
).with_headless_mode_overrides()
|
||||
agent = SerenaAgent(project=None, serena_config=config)
|
||||
try:
|
||||
assert agent.get_language_backend() == LanguageBackend.LSP
|
||||
assert agent.get_language_backend().is_lsp()
|
||||
finally:
|
||||
agent.on_shutdown(timeout=5)
|
||||
|
||||
def test_activate_project_rejects_backend_mismatch(self):
|
||||
"""Post-init activation of a project with mismatched backend raises ValueError."""
|
||||
# Start with LSP backend
|
||||
config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=LanguageBackend.LSP)
|
||||
config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=BuiltinLanguageBackend.LSP)
|
||||
|
||||
# Add a second project that requires JetBrains
|
||||
jb_project = Project(
|
||||
@@ -351,7 +353,7 @@ class TestEffectiveLanguageBackend:
|
||||
project_config=ProjectConfig(
|
||||
project_name="jb_proj",
|
||||
language_servers=[LanguageServerId.JAVA],
|
||||
language_backend=LanguageBackend.JETBRAINS,
|
||||
language_backend=BuiltinLanguageBackend.JETBRAINS.get_instance(),
|
||||
),
|
||||
serena_config=config,
|
||||
)
|
||||
@@ -366,14 +368,14 @@ class TestEffectiveLanguageBackend:
|
||||
|
||||
def test_activate_project_switches_backend_with_repl_interface(self):
|
||||
"""With the REPL interface, post-init activation of a project with a different backend switches the backend."""
|
||||
config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=LanguageBackend.LSP)
|
||||
config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=BuiltinLanguageBackend.LSP)
|
||||
config.agent_interface = AgentInterface.REPL
|
||||
jb_project = Project(
|
||||
project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "java" / "test_repo"),
|
||||
project_config=ProjectConfig(
|
||||
project_name="jb_proj",
|
||||
language_servers=[LanguageServerId.JAVA],
|
||||
language_backend=LanguageBackend.JETBRAINS,
|
||||
language_backend=BuiltinLanguageBackend.JETBRAINS.get_instance(),
|
||||
),
|
||||
serena_config=config,
|
||||
)
|
||||
@@ -381,12 +383,12 @@ class TestEffectiveLanguageBackend:
|
||||
|
||||
agent = SerenaAgent(project=name, serena_config=config)
|
||||
try:
|
||||
assert agent.get_language_backend() == LanguageBackend.LSP
|
||||
assert agent.get_language_backend().is_lsp()
|
||||
assert "s.lsp" in agent.get_repl().entrypoint.overview()
|
||||
|
||||
# the backend and everything depending on it follow the activated project
|
||||
agent.activate_project_from_path_or_name("jb_proj")
|
||||
assert agent.get_language_backend() == LanguageBackend.JETBRAINS
|
||||
assert agent.get_language_backend().is_jetbrains()
|
||||
overview = agent.get_repl().entrypoint.overview()
|
||||
assert "s.jb" in overview and "s.lsp" not in overview
|
||||
assert "jetbrains" in [m.name for m in agent.get_active_modes().get_modes(include_background_base_modes=True)]
|
||||
@@ -395,7 +397,7 @@ class TestEffectiveLanguageBackend:
|
||||
|
||||
def test_activate_project_allows_matching_backend(self):
|
||||
"""Post-init activation of a project with matching backend succeeds."""
|
||||
config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=LanguageBackend.LSP)
|
||||
config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=BuiltinLanguageBackend.LSP)
|
||||
|
||||
# Add a second project that also uses LSP
|
||||
lsp_project2 = Project(
|
||||
@@ -403,7 +405,7 @@ class TestEffectiveLanguageBackend:
|
||||
project_config=ProjectConfig(
|
||||
project_name="lsp_proj2",
|
||||
language_servers=[LanguageServerId.PYTHON],
|
||||
language_backend=LanguageBackend.LSP,
|
||||
language_backend=BuiltinLanguageBackend.LSP.get_instance(),
|
||||
),
|
||||
serena_config=config,
|
||||
)
|
||||
@@ -418,7 +420,7 @@ class TestEffectiveLanguageBackend:
|
||||
|
||||
def test_activate_project_allows_none_backend(self):
|
||||
"""Post-init activation of a project with no backend override succeeds."""
|
||||
config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=LanguageBackend.LSP)
|
||||
config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=BuiltinLanguageBackend.LSP)
|
||||
|
||||
# Add a second project with no backend override
|
||||
proj2 = Project(
|
||||
|
||||
@@ -18,6 +18,7 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from serena.code_editor import CodeEditor
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
from serena.util import file_system
|
||||
|
||||
|
||||
@@ -44,7 +45,14 @@ class _InMemoryEditedFile(CodeEditor.EditedFile):
|
||||
class _StubCodeEditor(CodeEditor[Any]):
|
||||
"""A ``CodeEditor`` whose only inherited behaviour under test is the file-saving path."""
|
||||
|
||||
class DummyProject:
|
||||
"""A dummy project object with only the attributes needed to construct a ``CodeEditor``."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.language_backend = BuiltinLanguageBackend.LSP.get_instance()
|
||||
|
||||
def __init__(self, project_root: str, encoding: str = "utf-8", newline: str | None = None) -> None:
|
||||
self.project = self.DummyProject()
|
||||
self.project_root = project_root
|
||||
self.encoding = encoding
|
||||
self.newline = newline
|
||||
|
||||
@@ -7,7 +7,8 @@ from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from serena.config.serena_config import ApiInclusionDefinition, LanguageBackend
|
||||
from serena.config.serena_config import ApiInclusionDefinition
|
||||
from serena.language_backend import BuiltinLanguageBackend
|
||||
from serena.repl.api.edit_api import EditApi
|
||||
from serena.repl.api.lsp_api import LspApi
|
||||
from serena.repl.external_project import ExternalProjectExecution
|
||||
@@ -83,10 +84,11 @@ class TestReplExecution:
|
||||
overview = rebuilt_repl.execute("facades()", session)
|
||||
assert "s.edit" in overview and "s.lsp" not in overview
|
||||
|
||||
@pytest.mark.parametrize("backend", [LanguageBackend.LSP, LanguageBackend.JETBRAINS])
|
||||
@pytest.mark.parametrize("builtin_backend", [BuiltinLanguageBackend.LSP, BuiltinLanguageBackend.JETBRAINS])
|
||||
@pytest.mark.parametrize("read_only", [True, False])
|
||||
def test_external_project_dispatch(self, backend: LanguageBackend, read_only: bool) -> None:
|
||||
def test_external_project_dispatch(self, builtin_backend: BuiltinLanguageBackend, read_only: bool) -> None:
|
||||
agent = MagicMock()
|
||||
backend = builtin_backend.get_instance()
|
||||
agent.get_language_backend.return_value = backend
|
||||
|
||||
class FakeExternalProject(ExternalProjectExecution):
|
||||
|
||||
Reference in new issue
Block a user