From 25139d1d74435b86ddd1c8ee6104e9be59d55049 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 24 Jul 2025 19:02:33 +0200 Subject: [PATCH 01/19] Improved performance of find_all_non_ignored_files by not descending into ignored dirs --- src/serena/util/file_system.py | 6 ++++-- src/serena/util/inspection.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index e4195aec..0999aef1 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -76,8 +76,10 @@ def find_all_non_ignored_files(repo_root: str) -> list[str]: :return: A list of all non-ignored files in the repository """ gitignore_parser = GitignoreParser(repo_root) - _, files = scan_directory(repo_root, recursive=True) - return [file for file in files if not gitignore_parser.should_ignore(file)] + _, files = scan_directory( + repo_root, recursive=True, is_ignored_dir=gitignore_parser.should_ignore, is_ignored_file=gitignore_parser.should_ignore + ) + return files @dataclass diff --git a/src/serena/util/inspection.py b/src/serena/util/inspection.py index 4a859c1f..668cf545 100644 --- a/src/serena/util/inspection.py +++ b/src/serena/util/inspection.py @@ -19,7 +19,7 @@ def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T], yield from iter_subclasses(subclass, recursive) -def determine_programming_language_composition(repo_path: str, rel_path_to_gitignore: str = ".gitignore") -> dict[str, float]: +def determine_programming_language_composition(repo_path: str) -> dict[str, float]: """ Determine the programming language composition of a repository. From 0abb8be85a69921ad45a59f9bf304d612931dfe3 Mon Sep 17 00:00:00 2001 From: Bingbing Zhang Date: Thu, 24 Jul 2025 18:10:53 +0100 Subject: [PATCH 02/19] Adds request_document_diagnostic to solidlsp (#347) --- src/solidlsp/ls.py | 47 ++++++++++++++++++- src/solidlsp/ls_types.py | 23 +++++++++ .../lsp_protocol_handler/lsp_constants.py | 6 +++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index d68331b0..3c1de8a5 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -153,7 +153,7 @@ class SolidLanguageServer(ABC): ls = CSharpLanguageServer(config, logger, repository_root_path) elif config.code_language == Language.CSHARP_OMNISHARP: - from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp + from solidlsp.language_servers.omnisharp import OmniSharp ls = OmniSharp(config, logger, repository_root_path) elif config.code_language == Language.TYPESCRIPT: @@ -709,6 +709,50 @@ class SolidLanguageServer(ABC): ret.append(ls_types.Location(**new_item)) return ret + + def request_text_document_diagnostics( + self, relative_file_path: str + ) -> list[ls_types.Diagnostic]: + """ + Raise a [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_diagnostic) request to the Language Server + to find diagnostics for the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file to retrieve diagnostics for + + :return: A list of diagnostics for the file + """ + if not self.server_started: + self.logger.log( + "request_text_document_diagnostics called before Language Server started", + logging.ERROR, + ) + raise LanguageServerException("Language Server not started") + + with self.open_file(relative_file_path): + response = self.server.send.text_document_diagnostic( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri() + } + } + ) + + if response is None: + return [] + + assert isinstance(response, dict), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}" + ret: list[ls_types.Diagnostic] = [] + for item in response["items"]: + new_item: ls_types.Diagnostic = { + "uri": pathlib.Path(str(PurePath(self.repository_root_path, relative_file_path))).as_uri(), + "severity": item["severity"], + "message": item["message"], + "range": item["range"], + "code": item["code"] + } + ret.append(ls_types.Diagnostic(new_item)) + + return ret def retrieve_full_file_content(self, file_path: str) -> str: """ @@ -1533,6 +1577,7 @@ class SolidLanguageServer(ABC): defining_symbol = self.request_containing_symbol(def_path, def_line, def_col, strict=False, include_body=include_body) return defining_symbol + @property def cache_path(self) -> Path: diff --git a/src/solidlsp/ls_types.py b/src/solidlsp/ls_types.py index 3d4d4658..6c9a2385 100644 --- a/src/solidlsp/ls_types.py +++ b/src/solidlsp/ls_types.py @@ -317,3 +317,26 @@ class Hover(TypedDict): range: NotRequired[Range] """ An optional range inside the text document that is used to visualize the hover, e.g. by changing the background color. """ + +class DiagnosticsSeverity(IntEnum): + ERROR = 1 + WARNING = 2 + INFORMATION = 3 + HINT = 4 + + +class Diagnostic(TypedDict): + """Diagnostic information for a text document.""" + + uri: DocumentUri + """ The URI of the text document to which the diagnostics apply. """ + range: Range + """ The range of the text document to which the diagnostics apply. """ + severity: NotRequired[DiagnosticsSeverity] + """ The severity of the diagnostic. """ + message: str + """ The diagnostic message. """ + code: str + """ The code of the diagnostic. """ + source: NotRequired[str] + """ The source of the diagnostic, e.g. the name of the tool that produced it. """ diff --git a/src/solidlsp/lsp_protocol_handler/lsp_constants.py b/src/solidlsp/lsp_protocol_handler/lsp_constants.py index 329c0039..467237dc 100644 --- a/src/solidlsp/lsp_protocol_handler/lsp_constants.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_constants.py @@ -61,3 +61,9 @@ class LSPConstants: # key used to represent the location in symbols LOCATION = "location" + + # Severity level of the diagnostic + SEVERITY = "severity" + + # The message of the diagnostic + MESSAGE = "message" From 81615c417605d156a80a96604f230eb15371728b Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Thu, 24 Jul 2025 19:45:24 +0200 Subject: [PATCH 03/19] Always ignore paths in .git --- CHANGELOG.md | 3 +++ src/serena/util/file_system.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e801958..89c1b190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +Fixes: +* Ignore `.git` in check for ignored paths and improve performance of `find_all_non_ignored_files` + # 0.1.3 ## Summary diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index 0999aef1..327a2236 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -3,6 +3,7 @@ import logging import os from collections.abc import Callable from dataclasses import dataclass, field +from pathlib import Path from typing import NamedTuple import pathspec @@ -258,6 +259,11 @@ class GitignoreParser: else: rel_path = path + # Ignore paths inside .git + rel_path_first_path = Path(rel_path).parts[0] + if rel_path_first_path == ".git": + return True + abs_path = os.path.join(self.repo_root, rel_path) # Normalize path separators From 1d65c854858c066a70decebad6be463b3f8c4271 Mon Sep 17 00:00:00 2001 From: Jaen Date: Thu, 24 Jul 2025 23:23:17 +0300 Subject: [PATCH 04/19] Fix gitignore parsing for anchored patterns and escaped characters - Move escaped character handling after negation pattern detection - Fix is_anchored logic to only check for leading "/" instead of any "/" - Properly remove directory markers with removesuffix("/") Addresses gitignore parsing issues mentioned in #348 --- src/serena/util/file_system.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index 327a2236..cf1baccd 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -185,10 +185,6 @@ class GitignoreParser: if not line or line.lstrip().startswith("#"): continue - # Handle escaped characters at the beginning - if line.startswith(("\\#", "\\!")): - line = line[1:] - # Store whether this is a negation pattern is_negation = line.startswith("!") if is_negation: @@ -200,13 +196,18 @@ class GitignoreParser: if not line: continue - # Determine if pattern is anchored to the gitignore directory - is_anchored = "/" in line[:-1] or line.startswith("/") - - # Remove leading slash for processing - if line.startswith("/"): + # Handle escaped characters at the beginning + if line.startswith(("\\#", "\\!")): line = line[1:] + # Determine if pattern is anchored to the gitignore directory and remove leading slash for processing + is_anchored = line.startswith("/") + if is_anchored: + line = line[1:] + + # Remove directory marker at the end since we do not support directory-only patterns + line = line.removesuffix("/") + # Adjust pattern based on gitignore file location if rel_dir: if is_anchored: From ba8f07d8366cddad29edd80ddab3e19ceff1edcf Mon Sep 17 00:00:00 2001 From: Jaen Date: Thu, 24 Jul 2025 23:35:54 +0300 Subject: [PATCH 05/19] Fix gitignore anchored pattern logic and update test - Use correct anchoring logic: only patterns starting with '/' are anchored - Fix test expectation for negation patterns to match Git behavior - Apply code formatting --- src/serena/util/file_system.py | 3 --- test/serena/util/test_file_system.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/serena/util/file_system.py b/src/serena/util/file_system.py index cf1baccd..4c667b0d 100644 --- a/src/serena/util/file_system.py +++ b/src/serena/util/file_system.py @@ -205,9 +205,6 @@ class GitignoreParser: if is_anchored: line = line[1:] - # Remove directory marker at the end since we do not support directory-only patterns - line = line.removesuffix("/") - # Adjust pattern based on gitignore file location if rel_dir: if is_anchored: diff --git a/test/serena/util/test_file_system.py b/test/serena/util/test_file_system.py index 3c9fd14e..97b9e3fd 100644 --- a/test/serena/util/test_file_system.py +++ b/test/serena/util/test_file_system.py @@ -369,7 +369,7 @@ debug.log assert "*.log" in patterns assert "!important.log" in patterns - assert "!/src/keep.log" in patterns + assert "!src/keep.log" in patterns def test_comments_and_empty_lines(self): """Test that comments and empty lines are ignored.""" From dcaf9ec992a528b15cf69ca0611608dca8aae11d Mon Sep 17 00:00:00 2001 From: Jaen Date: Thu, 24 Jul 2025 23:43:21 +0300 Subject: [PATCH 06/19] Add test for escaped negation pattern parsing Test demonstrates the fix for escaped character handling order: - Escaped negation patterns are treated as literals, not negations - Real negation patterns properly override ignore rules --- test/serena/util/test_file_system.py | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/serena/util/test_file_system.py b/test/serena/util/test_file_system.py index 97b9e3fd..2958a2c9 100644 --- a/test/serena/util/test_file_system.py +++ b/test/serena/util/test_file_system.py @@ -419,6 +419,38 @@ build/ assert "#not-a-comment.txt" in patterns assert "!not-negation.txt" in patterns + def test_escaped_negation_patterns(self): + test_dir = self.repo_path / "test_escaped_negation" + test_dir.mkdir() + + gitignore = test_dir / ".gitignore" + gitignore.write_text( + """*.log +\\!not-negation.log +!actual-negation.log +""" + ) + + parser = GitignoreParser(str(test_dir)) + specs = parser.get_ignore_specs() + + assert len(specs) == 1 + patterns = specs[0].patterns + + # Key assertions: escaped exclamation becomes literal, real negation preserved + assert "!not-negation.log" in patterns # escaped -> literal + assert "!actual-negation.log" in patterns # real negation preserved + + # Test the actual behavioral difference between escaped and real negation: + # *.log pattern should ignore test.log + assert parser.should_ignore("test.log") + + # Escaped negation file should still be ignored by *.log pattern + assert parser.should_ignore("!not-negation.log") + + # Actual negation should override the *.log pattern + assert not parser.should_ignore("actual-negation.log") + def test_glob_patterns(self): """Test various glob patterns work correctly.""" test_dir = self.repo_path / "test_glob" From ffca3478b7e94e39a29ff869bfc841be3624ce71 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 25 Jul 2025 12:43:37 +0200 Subject: [PATCH 07/19] Add file logging --- src/serena/cli.py | 11 +++++++++-- src/serena/config/serena_config.py | 28 +++++++++++++++++++++++++++- src/serena/constants.py | 2 +- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 444a0538..4c40ad55 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -12,7 +12,7 @@ from tqdm import tqdm from serena.agent import SerenaAgent from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode -from serena.config.serena_config import ProjectConfig, SerenaConfig +from serena.config.serena_config import ProjectConfig, SerenaConfig, SerenaPaths from serena.constants import ( DEFAULT_CONTEXT, DEFAULT_MODES, @@ -141,15 +141,22 @@ class TopLevelCommands(AutoRegisteringGroup): # initialize logging, using INFO level initially (will later be adjusted by SerenaAgent according to the config) # * memory log handler (for use by GUI/Dashboard) # * stream handler for stderr (for direct console output, which will also be captured by clients like Claude Desktop) + # * file handler # (Note that stdout must never be used for logging, as it is used by the MCP server to communicate with the client.) Logger.root.setLevel(logging.INFO) + formatter = logging.Formatter(SERENA_LOG_FORMAT) memory_log_handler = MemoryLogHandler() Logger.root.addHandler(memory_log_handler) stderr_handler = logging.StreamHandler(stream=sys.stderr) - stderr_handler.formatter = logging.Formatter(SERENA_LOG_FORMAT) + stderr_handler.formatter = formatter Logger.root.addHandler(stderr_handler) + log_path = SerenaPaths().get_next_log_file_path("mcp") + file_handler = logging.FileHandler(log_path, mode="w") + file_handler.formatter = formatter + Logger.root.addHandler(file_handler) log.info("Initializing Serena MCP server") + log.info("Storing logs in %s", log_path) project_file = project_file_arg or project factory = SerenaMCPFactorySingleProcess(context=context, project=project_file, memory_log_handler=memory_log_handler) server = factory.create_mcp_server( diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 29993a19..839f2f40 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -7,6 +7,7 @@ import shutil from collections.abc import Iterable from copy import deepcopy from dataclasses import dataclass, field +from datetime import datetime from functools import cached_property from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar @@ -14,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Optional, Self, TypeVar import yaml from ruamel.yaml.comments import CommentedMap from sensai.util import logging -from sensai.util.logging import LogTime +from sensai.util.logging import LogTime, datetime_tag from sensai.util.string import ToStringMixin from serena.constants import ( @@ -30,6 +31,7 @@ from serena.util.inspection import determine_programming_language_composition from solidlsp.ls_config import Language from ..analytics import RegisteredTokenCountEstimator +from ..util.class_decorators import singleton if TYPE_CHECKING: from ..project import Project @@ -39,6 +41,30 @@ T = TypeVar("T") DEFAULT_TOOL_TIMEOUT: float = 240 +@singleton +class SerenaPaths: + """ + Provides paths to various Serena-related directories and files. + """ + + def __init__(self): + self.user_config_dir: str = SERENA_MANAGED_DIR_IN_HOME + """ + the path to the user's Serena configuration directory, which is typically ~/.serena + """ + + def get_next_log_file_path(self, prefix: str): + """ + :param prefix: the filename prefix indicating the type of the log file + :return: the full path to the log file to use + """ + log_dir = os.path.join(self.user_config_dir, "logs", datetime.now().strftime("%Y-%m-%d")) + os.makedirs(log_dir, exist_ok=True) + return os.path.join(log_dir, prefix + "_" + datetime_tag() + ".txt") + + # TODO: Paths from constants.py should be moved here + + class ToolSet: def __init__(self, tool_names: set[str]) -> None: self._tool_names = tool_names diff --git a/src/serena/constants.py b/src/serena/constants.py index 4ed24bbb..9b0dd43f 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -8,6 +8,7 @@ _serena_in_home_managed_dir = Path.home() / ".serena" SERENA_MANAGED_DIR_IN_HOME = str(_serena_in_home_managed_dir) +# TODO: Path-related constants should be moved to SerenaPaths; don't add further constants here. REPO_ROOT = str(_repo_root_path) PROMPT_TEMPLATES_DIR = str(_serena_pkg_path / "resources" / "config" / "prompt_templates") SERENAS_OWN_CONTEXT_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "contexts") @@ -23,7 +24,6 @@ INTERNAL_MODE_YAMLS_DIR = str(_serena_pkg_path / "resources" / "config" / "inter SERENA_DASHBOARD_DIR = str(_serena_pkg_path / "resources" / "dashboard") SERENA_ICON_DIR = str(_serena_pkg_path / "resources" / "icons") - DEFAULT_ENCODING = "utf-8" DEFAULT_CONTEXT = "desktop-app" DEFAULT_MODES = ("interactive", "editing") From ce8356df2d62f71def5510b8fcfaa5b2f8c026ac Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 26 Jul 2025 14:17:49 +0200 Subject: [PATCH 08/19] CLI: command for listing tools [ci skip] --- src/serena/cli.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 444a0538..0db5e721 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -25,6 +25,7 @@ from serena.constants import ( ) from serena.mcp import SerenaMCPFactorySingleProcess from serena.project import Project +from serena.tools import ToolRegistry from serena.util.logging import MemoryLogHandler from solidlsp.ls_config import Language @@ -445,11 +446,33 @@ class ProjectCommands(AutoRegisteringGroup): print(f"Symbols saved to {ls.cache_path}") +class ToolCommands(AutoRegisteringGroup): + """Group for 'tool' subcommands.""" + + def __init__(self) -> None: + super().__init__( + name="tools", + help="Commands related to Serena's tools. You can run `serena tools --help` for more info on each command.", + ) + + @staticmethod + @click.command("list", help="Prints an overview of all tools implemented in Serena (not just the active ones for your project).") + @click.option("--quiet", "-q", multiple=True, is_flag=True) + def list(quiet: bool = False) -> None: + tool_registry = ToolRegistry() + if quiet: + for tool_name in tool_registry.get_tool_names_default_enabled(): + click.echo(tool_name) + else: + ToolRegistry().print_tool_overview() + + # Expose groups so we can reference them in pyproject.toml mode = ModeCommands() context = ContextCommands() project = ProjectCommands() config = SerenaConfigCommands() +tools = ToolCommands() # Expose toplevel commands for the same reason top_level = TopLevelCommands() @@ -457,7 +480,7 @@ start_mcp_server = top_level.start_mcp_server index_project = project.index_deprecated # needed for the help script to work - register all subcommands to the top-level group -for subgroup in (mode, context, project, config): +for subgroup in (mode, context, project, config, tools): top_level.add_command(subgroup) From 6e5f3d71650f72515fd57490ce56e044bc1de1bc Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 26 Jul 2025 14:29:33 +0200 Subject: [PATCH 09/19] CLI: allow -n for --name. Formatting. [ci skip] --- src/serena/cli.py | 4 ++++ src/solidlsp/ls.py | 9 +++------ src/solidlsp/ls_types.py | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 65ffeae3..9b10be5f 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -245,6 +245,8 @@ class ModeCommands(AutoRegisteringGroup): @click.command("create", help="Create a new mode or copy an internal one.") @click.option( "--name", + "-n", + multiple=True, type=str, default=None, help="Name for the new mode. If --from-internal is passed may be left empty to create a mode of the same name, which will then override the internal mode.", @@ -321,6 +323,8 @@ class ContextCommands(AutoRegisteringGroup): @click.command("create", help="Create a new context or copy an internal one.") @click.option( "--name", + "-n", + multiple=True, type=str, default=None, help="Name for the new context. If --from-internal is passed may be left empty to create a context of the same name, which will then override the internal context", diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 3c1de8a5..7cb82d72 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -709,10 +709,8 @@ class SolidLanguageServer(ABC): ret.append(ls_types.Location(**new_item)) return ret - - def request_text_document_diagnostics( - self, relative_file_path: str - ) -> list[ls_types.Diagnostic]: + + def request_text_document_diagnostics(self, relative_file_path: str) -> list[ls_types.Diagnostic]: """ Raise a [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_diagnostic) request to the Language Server to find diagnostics for the given file. Wait for the response and return the result. @@ -748,7 +746,7 @@ class SolidLanguageServer(ABC): "severity": item["severity"], "message": item["message"], "range": item["range"], - "code": item["code"] + "code": item["code"], } ret.append(ls_types.Diagnostic(new_item)) @@ -1577,7 +1575,6 @@ class SolidLanguageServer(ABC): defining_symbol = self.request_containing_symbol(def_path, def_line, def_col, strict=False, include_body=include_body) return defining_symbol - @property def cache_path(self) -> Path: diff --git a/src/solidlsp/ls_types.py b/src/solidlsp/ls_types.py index 6c9a2385..d8ff8903 100644 --- a/src/solidlsp/ls_types.py +++ b/src/solidlsp/ls_types.py @@ -318,6 +318,7 @@ class Hover(TypedDict): """ An optional range inside the text document that is used to visualize the hover, e.g. by changing the background color. """ + class DiagnosticsSeverity(IntEnum): ERROR = 1 WARNING = 2 From cc2ac851639953990910612509fd9c770433f1a3 Mon Sep 17 00:00:00 2001 From: MischaPanch Date: Sat, 26 Jul 2025 14:36:10 +0200 Subject: [PATCH 10/19] CLI: remove multiple=True (misunderstood purpose in last commit) [ci skip] --- src/serena/cli.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/serena/cli.py b/src/serena/cli.py index 9b10be5f..7591e0ca 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -246,7 +246,6 @@ class ModeCommands(AutoRegisteringGroup): @click.option( "--name", "-n", - multiple=True, type=str, default=None, help="Name for the new mode. If --from-internal is passed may be left empty to create a mode of the same name, which will then override the internal mode.", @@ -268,7 +267,7 @@ class ModeCommands(AutoRegisteringGroup): ) os.makedirs(os.path.dirname(dest), exist_ok=True) shutil.copyfile(src, dest) - click.echo(f"Created mode '{mode_name}' at {dest}.") + click.echo(f"Created mode '{mode_name}' at {dest}") _open_in_editor(dest) @staticmethod @@ -324,7 +323,6 @@ class ContextCommands(AutoRegisteringGroup): @click.option( "--name", "-n", - multiple=True, type=str, default=None, help="Name for the new context. If --from-internal is passed may be left empty to create a context of the same name, which will then override the internal context", @@ -346,7 +344,7 @@ class ContextCommands(AutoRegisteringGroup): ) os.makedirs(os.path.dirname(dest), exist_ok=True) shutil.copyfile(src, dest) - click.echo(f"Created context '{ctx_name}' at {dest}.") + click.echo(f"Created context '{ctx_name}' at {dest}") _open_in_editor(dest) @staticmethod @@ -468,7 +466,7 @@ class ToolCommands(AutoRegisteringGroup): @staticmethod @click.command("list", help="Prints an overview of all tools implemented in Serena (not just the active ones for your project).") - @click.option("--quiet", "-q", multiple=True, is_flag=True) + @click.option("--quiet", "-q", is_flag=True) def list(quiet: bool = False) -> None: tool_registry = ToolRegistry() if quiet: From bc621bb3a50bedcb0899b0b142e90362297df9f7 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sat, 26 Jul 2025 22:08:12 +0200 Subject: [PATCH 11/19] Improve logging of LS installations --- src/solidlsp/language_servers/common.py | 41 ++++++++++--------- .../typescript_language_server.py | 4 +- src/solidlsp/ls_utils.py | 3 ++ 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index a0973e56..58a1f1b9 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import os import subprocess from collections.abc import Sequence @@ -8,6 +9,8 @@ from dataclasses import dataclass from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PlatformUtils +log = logging.getLogger(__name__) + @dataclass(kw_only=True) class RuntimeDependency: @@ -69,28 +72,28 @@ class RuntimeDependencyCollection: @staticmethod def _run_command(command: str, cwd: str) -> None: - if PlatformUtils.get_platform_id().value.startswith("win"): - subprocess.run( - command, - shell=True, - check=True, - cwd=cwd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + kwargs = {} + if PlatformUtils.get_platform_id().is_windows(): + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore else: import pwd - user = pwd.getpwuid(os.getuid()).pw_name - subprocess.run( - command, - shell=True, - check=True, - user=user, - cwd=cwd, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name + log.info("Running command '%s' in '%s'", command, cwd) + completed_proces = subprocess.run( + command, + shell=True, + check=True, + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + **kwargs, + ) + log.log( + logging.WARNING if completed_proces.returncode else logging.INFO, + "Command completed with return code %d", + completed_proces.returncode, + ) @staticmethod def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None: diff --git a/src/solidlsp/language_servers/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server.py index f994f95f..48bcf444 100644 --- a/src/solidlsp/language_servers/typescript_language_server.py +++ b/src/solidlsp/language_servers/typescript_language_server.py @@ -10,6 +10,7 @@ import threading from time import sleep from overrides import override +from sensai.util.logging import LogTime from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig @@ -117,7 +118,8 @@ class TypeScriptLanguageServer(SolidLanguageServer): tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") if not os.path.exists(tsserver_executable_path): logger.log(f"Typescript Language Server executable not found at {tsserver_executable_path}. Installing...", logging.INFO) - deps.install(logger, tsserver_ls_dir) + with LogTime("Installation of TypeScript language server dependencies", logger=logger.logger): + deps.install(logger, tsserver_ls_dir) if not os.path.exists(tsserver_executable_path): raise FileNotFoundError( diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index b46491ec..d1161880 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -247,6 +247,9 @@ class PlatformId(str, Enum): LINUX_MUSL_x64 = "linux-musl-x64" LINUX_MUSL_arm64 = "linux-musl-arm64" + def is_windows(self): + return self.value.startswith("win") + class DotnetVersion(str, Enum): """ From 9c83cf74f4dc7b7b9d4d8a20dc0b62e13a83a5b4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 11:00:40 +0200 Subject: [PATCH 12/19] Further improve logging of dependency installation --- src/solidlsp/language_servers/common.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/solidlsp/language_servers/common.py b/src/solidlsp/language_servers/common.py index 58a1f1b9..4ce8f3b4 100644 --- a/src/solidlsp/language_servers/common.py +++ b/src/solidlsp/language_servers/common.py @@ -80,7 +80,7 @@ class RuntimeDependencyCollection: kwargs["user"] = pwd.getpwuid(os.getuid()).pw_name log.info("Running command '%s' in '%s'", command, cwd) - completed_proces = subprocess.run( + completed_process = subprocess.run( command, shell=True, check=True, @@ -89,11 +89,13 @@ class RuntimeDependencyCollection: stderr=subprocess.STDOUT, **kwargs, ) - log.log( - logging.WARNING if completed_proces.returncode else logging.INFO, - "Command completed with return code %d", - completed_proces.returncode, - ) + if completed_process.returncode != 0: + log.warning("Command '%s' failed with return code %d", command, completed_process.returncode) + log.warning("Command output:\n%s", completed_process.stdout) + else: + log.info( + "Command completed successfully", + ) @staticmethod def _install_from_url(dep: RuntimeDependency, logger: LanguageServerLogger, target_dir: str) -> None: From a8078bbc5230cb059ff04b45fe28ee8ad1d8c518 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 27 Jul 2025 16:01:03 +0700 Subject: [PATCH 13/19] docs(readme): fix wrong sse url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a349aef1..788ac9b6 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ you would run this command from the Serena directory, uv run serena-mcp-server --transport sse --port 9121 ``` -and then configure your client to connect to `http://localhost/sse:9121`. +and then configure your client to connect to `http://localhost:9121/sse`. #### Command-Line Arguments From f55eb0c9b438beed1bae87d19d961bb2de88855e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 13:26:38 +0200 Subject: [PATCH 14/19] Add type annotations --- src/serena/config/serena_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 839f2f40..41ef6433 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -47,13 +47,13 @@ class SerenaPaths: Provides paths to various Serena-related directories and files. """ - def __init__(self): + def __init__(self) -> None: self.user_config_dir: str = SERENA_MANAGED_DIR_IN_HOME """ the path to the user's Serena configuration directory, which is typically ~/.serena """ - def get_next_log_file_path(self, prefix: str): + def get_next_log_file_path(self, prefix: str) -> str: """ :param prefix: the filename prefix indicating the type of the log file :return: the full path to the log file to use From 0e890407182ed1cc51710539a68228d2a9b9f922 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 13:25:07 +0200 Subject: [PATCH 15/19] Use strategy pattern instead of higher-order function in LanguageServerRequest --- src/solidlsp/ls_handler.py | 2 +- src/solidlsp/ls_request.py | 112 +++++++++++++++++++------------------ 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 731952f3..9fd65823 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -104,7 +104,7 @@ class SolidLanguageServerHandler: start_independent_lsp_process=True, request_timeout: float | None = None, ) -> None: - self.send = LanguageServerRequest(self.send_request) + self.send = LanguageServerRequest(self) self.notify = LspNotification(self.send_notification) self.process_launch_info = process_launch_info diff --git a/src/solidlsp/ls_request.py b/src/solidlsp/ls_request.py index 209a3f04..f81727d4 100644 --- a/src/solidlsp/ls_request.py +++ b/src/solidlsp/ls_request.py @@ -1,11 +1,15 @@ -from typing import Union +from typing import Any, Union +from solidlsp.ls_handler import SolidLanguageServerHandler from solidlsp.lsp_protocol_handler import lsp_types class LanguageServerRequest: - def __init__(self, send_request): - self.send_request = send_request + def __init__(self, handler: SolidLanguageServerHandler): + self.handler = handler + + def _send_request(self, method: str, params: Any | None = None) -> Any: + return self.handler.send_request(method, params) def implementation(self, params: lsp_types.ImplementationParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the implementation locations of a symbol at a given text @@ -13,7 +17,7 @@ class LanguageServerRequest: (#TextDocumentPositionParams) the response is of type {@link Definition} or a Thenable that resolves to such. """ - return self.send_request("textDocument/implementation", params) + return self._send_request("textDocument/implementation", params) def type_definition( self, params: lsp_types.TypeDefinitionParams @@ -23,7 +27,7 @@ class LanguageServerRequest: (#TextDocumentPositionParams) the response is of type {@link Definition} or a Thenable that resolves to such. """ - return self.send_request("textDocument/typeDefinition", params) + return self._send_request("textDocument/typeDefinition", params) def document_color(self, params: lsp_types.DocumentColorParams) -> list["lsp_types.ColorInformation"]: """A request to list all color symbols found in a given text document. The request's @@ -31,7 +35,7 @@ class LanguageServerRequest: response is of type {@link ColorInformation ColorInformation[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/documentColor", params) + return self._send_request("textDocument/documentColor", params) def color_presentation(self, params: lsp_types.ColorPresentationParams) -> list["lsp_types.ColorPresentation"]: """A request to list all presentation for a color. The request's @@ -39,7 +43,7 @@ class LanguageServerRequest: response is of type {@link ColorInformation ColorInformation[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/colorPresentation", params) + return self._send_request("textDocument/colorPresentation", params) def folding_range(self, params: lsp_types.FoldingRangeParams) -> list["lsp_types.FoldingRange"] | None: """A request to provide folding ranges in a document. The request's @@ -47,7 +51,7 @@ class LanguageServerRequest: response is of type {@link FoldingRangeList} or a Thenable that resolves to such. """ - return self.send_request("textDocument/foldingRange", params) + return self._send_request("textDocument/foldingRange", params) def declaration(self, params: lsp_types.DeclarationParams) -> Union["lsp_types.Declaration", list["lsp_types.LocationLink"], None]: """A request to resolve the type definition locations of a symbol at a given text @@ -56,7 +60,7 @@ class LanguageServerRequest: or a typed array of {@link DeclarationLink} or a Thenable that resolves to such. """ - return self.send_request("textDocument/declaration", params) + return self._send_request("textDocument/declaration", params) def selection_range(self, params: lsp_types.SelectionRangeParams) -> list["lsp_types.SelectionRange"] | None: """A request to provide selection ranges in a document. The request's @@ -64,7 +68,7 @@ class LanguageServerRequest: response is of type {@link SelectionRange SelectionRange[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/selectionRange", params) + return self._send_request("textDocument/selectionRange", params) def prepare_call_hierarchy(self, params: lsp_types.CallHierarchyPrepareParams) -> list["lsp_types.CallHierarchyItem"] | None: """A request to result a `CallHierarchyItem` in a document at a given position. @@ -72,42 +76,42 @@ class LanguageServerRequest: @since 3.16.0 """ - return self.send_request("textDocument/prepareCallHierarchy", params) + return self._send_request("textDocument/prepareCallHierarchy", params) def incoming_calls(self, params: lsp_types.CallHierarchyIncomingCallsParams) -> list["lsp_types.CallHierarchyIncomingCall"] | None: """A request to resolve the incoming calls for a given `CallHierarchyItem`. @since 3.16.0 """ - return self.send_request("callHierarchy/incomingCalls", params) + return self._send_request("callHierarchy/incomingCalls", params) def outgoing_calls(self, params: lsp_types.CallHierarchyOutgoingCallsParams) -> list["lsp_types.CallHierarchyOutgoingCall"] | None: """A request to resolve the outgoing calls for a given `CallHierarchyItem`. @since 3.16.0 """ - return self.send_request("callHierarchy/outgoingCalls", params) + return self._send_request("callHierarchy/outgoingCalls", params) def semantic_tokens_full(self, params: lsp_types.SemanticTokensParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" - return self.send_request("textDocument/semanticTokens/full", params) + return self._send_request("textDocument/semanticTokens/full", params) def semantic_tokens_delta( self, params: lsp_types.SemanticTokensDeltaParams ) -> Union["lsp_types.SemanticTokens", "lsp_types.SemanticTokensDelta", None]: """@since 3.16.0""" - return self.send_request("textDocument/semanticTokens/full/delta", params) + return self._send_request("textDocument/semanticTokens/full/delta", params) def semantic_tokens_range(self, params: lsp_types.SemanticTokensRangeParams) -> Union["lsp_types.SemanticTokens", None]: """@since 3.16.0""" - return self.send_request("textDocument/semanticTokens/range", params) + return self._send_request("textDocument/semanticTokens/range", params) def linked_editing_range(self, params: lsp_types.LinkedEditingRangeParams) -> Union["lsp_types.LinkedEditingRanges", None]: """A request to provide ranges that can be edited together. @since 3.16.0 """ - return self.send_request("textDocument/linkedEditingRange", params) + return self._send_request("textDocument/linkedEditingRange", params) def will_create_files(self, params: lsp_types.CreateFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will create files request is sent from the client to the server before files are actually @@ -115,7 +119,7 @@ class LanguageServerRequest: @since 3.16.0 """ - return self.send_request("workspace/willCreateFiles", params) + return self._send_request("workspace/willCreateFiles", params) def will_rename_files(self, params: lsp_types.RenameFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The will rename files request is sent from the client to the server before files are actually @@ -123,7 +127,7 @@ class LanguageServerRequest: @since 3.16.0 """ - return self.send_request("workspace/willRenameFiles", params) + return self._send_request("workspace/willRenameFiles", params) def will_delete_files(self, params: lsp_types.DeleteFilesParams) -> Union["lsp_types.WorkspaceEdit", None]: """The did delete files notification is sent from the client to the server when @@ -131,14 +135,14 @@ class LanguageServerRequest: @since 3.16.0 """ - return self.send_request("workspace/willDeleteFiles", params) + return self._send_request("workspace/willDeleteFiles", params) def moniker(self, params: lsp_types.MonikerParams) -> list["lsp_types.Moniker"] | None: """A request to get the moniker of a symbol at a given text document position. The request parameter is of type {@link TextDocumentPositionParams}. The response is of type {@link Moniker Moniker[]} or `null`. """ - return self.send_request("textDocument/moniker", params) + return self._send_request("textDocument/moniker", params) def prepare_type_hierarchy(self, params: lsp_types.TypeHierarchyPrepareParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to result a `TypeHierarchyItem` in a document at a given position. @@ -146,21 +150,21 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("textDocument/prepareTypeHierarchy", params) + return self._send_request("textDocument/prepareTypeHierarchy", params) def type_hierarchy_supertypes(self, params: lsp_types.TypeHierarchySupertypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the supertypes for a given `TypeHierarchyItem`. @since 3.17.0 """ - return self.send_request("typeHierarchy/supertypes", params) + return self._send_request("typeHierarchy/supertypes", params) def type_hierarchy_subtypes(self, params: lsp_types.TypeHierarchySubtypesParams) -> list["lsp_types.TypeHierarchyItem"] | None: """A request to resolve the subtypes for a given `TypeHierarchyItem`. @since 3.17.0 """ - return self.send_request("typeHierarchy/subtypes", params) + return self._send_request("typeHierarchy/subtypes", params) def inline_value(self, params: lsp_types.InlineValueParams) -> list["lsp_types.InlineValue"] | None: """A request to provide inline values in a document. The request's parameter is of @@ -169,7 +173,7 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("textDocument/inlineValue", params) + return self._send_request("textDocument/inlineValue", params) def inlay_hint(self, params: lsp_types.InlayHintParams) -> list["lsp_types.InlayHint"] | None: """A request to provide inlay hints in a document. The request's parameter is of @@ -178,7 +182,7 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("textDocument/inlayHint", params) + return self._send_request("textDocument/inlayHint", params) def resolve_inlay_hint(self, params: lsp_types.InlayHint) -> "lsp_types.InlayHint": """A request to resolve additional properties for an inlay hint. @@ -187,21 +191,21 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("inlayHint/resolve", params) + return self._send_request("inlayHint/resolve", params) def text_document_diagnostic(self, params: lsp_types.DocumentDiagnosticParams) -> "lsp_types.DocumentDiagnosticReport": """The document diagnostic request definition. @since 3.17.0 """ - return self.send_request("textDocument/diagnostic", params) + return self._send_request("textDocument/diagnostic", params) def workspace_diagnostic(self, params: lsp_types.WorkspaceDiagnosticParams) -> "lsp_types.WorkspaceDiagnosticReport": """The workspace diagnostic request definition. @since 3.17.0 """ - return self.send_request("workspace/diagnostic", params) + return self._send_request("workspace/diagnostic", params) def initialize(self, params: lsp_types.InitializeParams) -> "lsp_types.InitializeResult": """The initialize request is sent from the client to the server. @@ -210,7 +214,7 @@ class LanguageServerRequest: the response if of type {@link InitializeResult} of a Thenable that resolves to such. """ - return self.send_request("initialize", params) + return self._send_request("initialize", params) def shutdown(self) -> None: """A shutdown request is sent from the client to the server. @@ -218,7 +222,7 @@ class LanguageServerRequest: server. The only notification that is sent after a shutdown request is the exit event. """ - return self.send_request("shutdown") + return self._send_request("shutdown") def will_save_wait_until(self, params: lsp_types.WillSaveTextDocumentParams) -> list["lsp_types.TextEdit"] | None: """A document will save request is sent from the client to the server before @@ -228,7 +232,7 @@ class LanguageServerRequest: server constantly fails on this request. This is done to keep the save fast and reliable. """ - return self.send_request("textDocument/willSaveWaitUntil", params) + return self._send_request("textDocument/willSaveWaitUntil", params) def completion(self, params: lsp_types.CompletionParams) -> Union[list["lsp_types.CompletionItem"], "lsp_types.CompletionList", None]: """Request to request completion at a given text document position. The request's @@ -241,24 +245,24 @@ class LanguageServerRequest: request. However, properties that are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `textEdit`, must not be changed during resolve. """ - return self.send_request("textDocument/completion", params) + return self._send_request("textDocument/completion", params) def resolve_completion_item(self, params: lsp_types.CompletionItem) -> "lsp_types.CompletionItem": """Request to resolve additional information for a given completion item.The request's parameter is of type {@link CompletionItem} the response is of type {@link CompletionItem} or a Thenable that resolves to such. """ - return self.send_request("completionItem/resolve", params) + return self._send_request("completionItem/resolve", params) def hover(self, params: lsp_types.HoverParams) -> Union["lsp_types.Hover", None]: """Request to request hover information at a given text document position. The request's parameter is of type {@link TextDocumentPosition} the response is of type {@link Hover} or a Thenable that resolves to such. """ - return self.send_request("textDocument/hover", params) + return self._send_request("textDocument/hover", params) def signature_help(self, params: lsp_types.SignatureHelpParams) -> Union["lsp_types.SignatureHelp", None]: - return self.send_request("textDocument/signatureHelp", params) + return self._send_request("textDocument/signatureHelp", params) def definition(self, params: lsp_types.DefinitionParams) -> Union["lsp_types.Definition", list["lsp_types.LocationLink"], None]: """A request to resolve the definition location of a symbol at a given text @@ -267,7 +271,7 @@ class LanguageServerRequest: or a typed array of {@link DefinitionLink} or a Thenable that resolves to such. """ - return self.send_request("textDocument/definition", params) + return self._send_request("textDocument/definition", params) def references(self, params: lsp_types.ReferenceParams) -> list["lsp_types.Location"] | None: """A request to resolve project-wide references for the symbol denoted @@ -275,7 +279,7 @@ class LanguageServerRequest: type {@link ReferenceParams} the response is of type {@link Location Location[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/references", params) + return self._send_request("textDocument/references", params) def document_highlight(self, params: lsp_types.DocumentHighlightParams) -> list["lsp_types.DocumentHighlight"] | None: """Request to resolve a {@link DocumentHighlight} for a given @@ -283,7 +287,7 @@ class LanguageServerRequest: (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] (#DocumentHighlight) or a Thenable that resolves to such. """ - return self.send_request("textDocument/documentHighlight", params) + return self._send_request("textDocument/documentHighlight", params) def document_symbol( self, params: lsp_types.DocumentSymbolParams @@ -293,18 +297,18 @@ class LanguageServerRequest: response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable that resolves to such. """ - return self.send_request("textDocument/documentSymbol", params) + return self._send_request("textDocument/documentSymbol", params) def code_action(self, params: lsp_types.CodeActionParams) -> list[Union["lsp_types.Command", "lsp_types.CodeAction"]] | None: """A request to provide commands for the given text document and range.""" - return self.send_request("textDocument/codeAction", params) + return self._send_request("textDocument/codeAction", params) def resolve_code_action(self, params: lsp_types.CodeAction) -> "lsp_types.CodeAction": """Request to resolve additional information for a given code action.The request's parameter is of type {@link CodeAction} the response is of type {@link CodeAction} or a Thenable that resolves to such. """ - return self.send_request("codeAction/resolve", params) + return self._send_request("codeAction/resolve", params) def workspace_symbol( self, params: lsp_types.WorkspaceSymbolParams @@ -318,7 +322,7 @@ class LanguageServerRequest: need to advertise support for WorkspaceSymbols via the client capability `workspace.symbol.resolveSupport`. """ - return self.send_request("workspace/symbol", params) + return self._send_request("workspace/symbol", params) def resolve_workspace_symbol(self, params: lsp_types.WorkspaceSymbol) -> "lsp_types.WorkspaceSymbol": """A request to resolve the range inside the workspace @@ -326,52 +330,52 @@ class LanguageServerRequest: @since 3.17.0 """ - return self.send_request("workspaceSymbol/resolve", params) + return self._send_request("workspaceSymbol/resolve", params) def code_lens(self, params: lsp_types.CodeLensParams) -> list["lsp_types.CodeLens"] | None: """A request to provide code lens for the given text document.""" - return self.send_request("textDocument/codeLens", params) + return self._send_request("textDocument/codeLens", params) def resolve_code_lens(self, params: lsp_types.CodeLens) -> "lsp_types.CodeLens": """A request to resolve a command for a given code lens.""" - return self.send_request("codeLens/resolve", params) + return self._send_request("codeLens/resolve", params) def document_link(self, params: lsp_types.DocumentLinkParams) -> list["lsp_types.DocumentLink"] | None: """A request to provide document links""" - return self.send_request("textDocument/documentLink", params) + return self._send_request("textDocument/documentLink", params) def resolve_document_link(self, params: lsp_types.DocumentLink) -> "lsp_types.DocumentLink": """Request to resolve additional information for a given document link. The request's parameter is of type {@link DocumentLink} the response is of type {@link DocumentLink} or a Thenable that resolves to such. """ - return self.send_request("documentLink/resolve", params) + return self._send_request("documentLink/resolve", params) def formatting(self, params: lsp_types.DocumentFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a whole document.""" - return self.send_request("textDocument/formatting", params) + return self._send_request("textDocument/formatting", params) def range_formatting(self, params: lsp_types.DocumentRangeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to to format a range in a document.""" - return self.send_request("textDocument/rangeFormatting", params) + return self._send_request("textDocument/rangeFormatting", params) def on_type_formatting(self, params: lsp_types.DocumentOnTypeFormattingParams) -> list["lsp_types.TextEdit"] | None: """A request to format a document on type.""" - return self.send_request("textDocument/onTypeFormatting", params) + return self._send_request("textDocument/onTypeFormatting", params) def rename(self, params: lsp_types.RenameParams) -> Union["lsp_types.WorkspaceEdit", None]: """A request to rename a symbol.""" - return self.send_request("textDocument/rename", params) + return self._send_request("textDocument/rename", params) def prepare_rename(self, params: lsp_types.PrepareRenameParams) -> Union["lsp_types.PrepareRenameResult", None]: """A request to test and perform the setup necessary for a rename. @since 3.16 - support for default behavior """ - return self.send_request("textDocument/prepareRename", params) + return self._send_request("textDocument/prepareRename", params) def execute_command(self, params: lsp_types.ExecuteCommandParams) -> Union["lsp_types.LSPAny", None]: """A request send from the client to the server to execute a command. The request might return a workspace edit which the client will apply to the workspace. """ - return self.send_request("workspace/executeCommand", params) + return self._send_request("workspace/executeCommand", params) From 6fbea2dfe966ac26918f953751938bb5fdc40753 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 13:31:45 +0200 Subject: [PATCH 16/19] Remove obsolete termination handling --- src/solidlsp/ls_handler.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 9fd65823..5e6754be 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -225,21 +225,6 @@ class SolidLanguageServerHandler: # First try to terminate the process tree gracefully self._signal_process_tree(process, terminate=True) - # TODO - """ - # Wait for the process to exit (with timeout) - try: - asyncio.wait_for(process.wait(), timeout=10) - except (asyncio.TimeoutError, Exception): - # If termination failed, forcefully kill the process tree - self._signal_process_tree(process, terminate=False) - try: - # Give it one more chance to exit - await asyncio.wait_for(process.wait(), timeout=2) - except Exception: - pass - """ - def _signal_process_tree(self, process, terminate=True): """Send signal (terminate or kill) to the process and all its children.""" signal_method = "terminate" if terminate else "kill" @@ -283,14 +268,6 @@ class SolidLanguageServerHandler: self._log("Sending exit notification to server") self.notify.exit() self._log("Sent exit notification to server") - # TODO - """ - if self.process and self.process.stdout: - self.process.stdout.set_exception(StopLoopException()) - # This yields the control to the event loop to allow the exception to be handled - # in the run_forever and run_forever_stderr methods - await asyncio.sleep(0) - """ def _log(self, message: str | StringDict) -> None: """ From a15833b22d3790927894a3717f04fa8d31ce7c44 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 18:24:03 +0200 Subject: [PATCH 17/19] Improve error handling in language server communication * Introduce appropriate exceptions that allow errors to be sufficiently identified * If an error occurs in the read thread of the LS output stream, cancel all pending requests, setting an appropriate error * Propagate error information all the way to the original caller (in the tool code): If errors are due to LS termination, restart the server and retry --- src/serena/tools/tools_base.py | 22 ++++--- src/solidlsp/ls_exceptions.py | 37 +++++++++-- src/solidlsp/ls_handler.py | 111 +++++++++++++++++++++++---------- src/solidlsp/ls_request.py | 8 ++- 4 files changed, 128 insertions(+), 50 deletions(-) diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 173726fc..c8163390 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -1,6 +1,5 @@ import inspect import os -import traceback from abc import ABC from collections.abc import Iterable from dataclasses import dataclass @@ -16,6 +15,7 @@ from serena.prompt_factory import PromptFactory from serena.symbol import LanguageServerSymbolRetriever from serena.util.class_decorators import singleton from serena.util.inspection import iter_subclasses +from solidlsp.ls_exceptions import LanguageServerException if TYPE_CHECKING: from serena.agent import LinesRead, MemoriesManager, SerenaAgent @@ -237,18 +237,24 @@ class Tool(Component): self.agent.reset_language_server() # apply the actual tool - result = apply_fn(**kwargs) + try: + result = apply_fn(**kwargs) + except LanguageServerException as e: + if e.is_language_server_terminated(): + log.error(f"Language server terminated while executing tool ({e}). Restarting the language server and retrying ...") + self.agent.reset_language_server() + result = apply_fn(**kwargs) + else: + raise + + # record tool usage self.agent.record_tool_usage_if_enabled(kwargs, result, self) except Exception as e: if not catch_exceptions: raise - msg = f"Error executing tool: {e}\n{traceback.format_exc()}" - log.error( - f"Error executing tool: {e}. " - f"Consider restarting the language server to solve this (especially, if it's a timeout of a symbolic operation)", - exc_info=e, - ) + msg = f"Error executing tool: {e}" + log.error(f"Error executing tool: {e}", exc_info=e) result = msg if log_call: diff --git a/src/solidlsp/ls_exceptions.py b/src/solidlsp/ls_exceptions.py index 0484da6b..a3bb0f11 100644 --- a/src/solidlsp/ls_exceptions.py +++ b/src/solidlsp/ls_exceptions.py @@ -1,15 +1,40 @@ """ -This module contains the exceptions raised by the Multilspy framework. +This module contains the exceptions raised by the framework. """ class LanguageServerException(Exception): - """ - Exceptions raised by the Multilspy framework. - """ - - def __init__(self, message: str): + def __init__(self, message: str, cause: Exception | None = None): """ Initializes the exception with the given message. + + :param message: the message describing the exception + :param cause: the original exception that caused this exception, if any. + For exceptions raised during request handling, this is typically + * an LSPError for errors returned by the LSP server + * LanguageServerTerminatedException for errors due to the language server having terminated. """ + self.cause = cause super().__init__(message) + + def is_language_server_terminated(self): + """ + :return: True if the exception is caused by the language server having terminated as indicated + by the causing exception being an instance of LanguageServerTerminatedException. + """ + from .ls_handler import LanguageServerTerminatedException + + return isinstance(self.cause, LanguageServerTerminatedException) + + def __str__(self) -> str: + """ + Returns a string representation of the exception. + """ + s = super().__str__() + if self.cause: + if "\n" in s: + s += "\n" + else: + s += " " + s += f"(caused by {self.cause})" + return s diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index 5e6754be..ab8a6772 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -12,6 +12,7 @@ from queue import Queue from typing import Any import psutil +from sensai.util.string import ToStringMixin from solidlsp.ls_exceptions import LanguageServerException from solidlsp.ls_request import LanguageServerRequest @@ -35,23 +36,50 @@ from solidlsp.lsp_protocol_handler.server import ( log = logging.getLogger(__name__) -class Request: +class LanguageServerTerminatedException(Exception): + """ + Exception raised when the language server process has terminated unexpectedly. + """ + + def __init__(self, message: str, cause: Exception | None = None) -> None: + super().__init__(message) + self.message = message + self.cause = cause + + def __str__(self) -> str: + return f"LanguageServerTerminatedException: {self.message}" + (f"; Cause: {self.cause}" if self.cause else "") + + +class Request(ToStringMixin): @dataclass class Result: payload: PayloadLike | None = None - error: Error | None = None + error: Exception | None = None def is_error(self) -> bool: return self.error is not None - def __init__(self) -> None: + def __init__(self, request_id: int, method: str) -> None: + self._request_id = request_id + self._method = method + self._status = "pending" self._result_queue = Queue() + def _tostring_includes(self) -> list[str]: + return ["_request_id", "_status", "_method"] + def on_result(self, params: PayloadLike) -> None: + self._status = "completed" self._result_queue.put(Request.Result(payload=params)) - def on_error(self, err: Error) -> None: + def on_error(self, err: Exception) -> None: + """ + :param err: the error that occurred while processing the request (typically an LSPError + for errors returned by the LS or LanguageServerTerminatedException if the error + is due to the language server process terminating unexpectedly). + """ + self._status = "error" self._result_queue.put(Request.Result(error=err)) def get_result(self, timeout: float | None = None) -> Result: @@ -75,10 +103,8 @@ class SolidLanguageServerHandler: notify: A LspNotification object that can be used to send notifications to the server. cmd: A string that represents the command to launch the language server process. process: A subprocess.Popen object that represents the language server process. - _received_shutdown: A boolean flag that indicates whether the client has received - a shutdown request from the server. request_id: An integer that represents the next available request id for the client. - _response_handlers: A dictionary that maps request ids to Request objects that + _pending_requests: A dictionary that maps request ids to Request objects that store the results or errors of the requests. on_request_handlers: A dictionary that maps method names to callback functions that handle requests from the server. @@ -109,10 +135,10 @@ class SolidLanguageServerHandler: self.process_launch_info = process_launch_info self.process = None - self._received_shutdown = False + self._is_shutting_down = False self.request_id = 1 - self._response_handlers: dict[Any, Request] = {} + self._pending_requests: dict[Any, Request] = {} self.on_request_handlers = {} self.on_notification_handlers = {} self.logger = logger @@ -176,12 +202,12 @@ class SolidLanguageServerHandler: # start threads to read stdout and stderr of the process threading.Thread( - target=self.run_forever, + target=self._read_ls_process_stdout, name="LSP-stdout-reader", daemon=True, ).start() threading.Thread( - target=self.run_forever_stderr, + target=self._read_ls_process_stderr, name="LSP-stderr-reader", daemon=True, ).start() @@ -261,10 +287,10 @@ class SolidLanguageServerHandler: """ Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit """ + self._is_shutting_down = True self._log("Sending shutdown request to server") self.send.shutdown() self._log("Received shutdown response from server") - self._received_shutdown = True self._log("Sending exit notification to server") self.notify.exit() self._log("Sent exit notification to server") @@ -279,27 +305,26 @@ class SolidLanguageServerHandler: @staticmethod def _read_bytes_from_process(process, stream, num_bytes): """Read exactly num_bytes from process stdout""" - if process.poll() is not None: - # Process has terminated, check if we can still read - pass - data = b"" while len(data) < num_bytes: chunk = stream.read(num_bytes - len(data)) if not chunk: if process.poll() is not None: - raise EOFError(f"Process terminated. Expected {num_bytes} bytes, got {len(data)}") - # Process still running but no data available yet - time.sleep(0.01) # Small delay + raise LanguageServerTerminatedException( + f"Process terminated while trying to read response (read {num_bytes} of {len(data)} bytes before termination)" + ) + # Process still running but no data available yet, retry after a short delay + time.sleep(0.01) continue data += chunk return data - def run_forever(self) -> bool: + def _read_ls_process_stdout(self) -> None: """ Continuously read from the language server process stdout and handle the messages invoking the registered response and notification handlers """ + exception: Exception | None = None try: while self.process and self.process.stdout and self.process.stdout.readable(): line = self.process.stdout.readline() @@ -318,11 +343,20 @@ class SolidLanguageServerHandler: body = self._read_bytes_from_process(self.process, self.process.stdout, num_bytes) self._handle_body(body) - except (BrokenPipeError, ConnectionResetError): - pass - return self._received_shutdown + except LanguageServerTerminatedException as e: + exception = e + except (BrokenPipeError, ConnectionResetError) as e: + exception = LanguageServerTerminatedException("Language server process terminated while reading stdout", cause=e) + except Exception as e: + exception = LanguageServerTerminatedException("Unexpected error while reading stdout from language server process", cause=e) + log.info("Language server stdout reader thread has terminated") + if not self._is_shutting_down: + if exception is None: + exception = LanguageServerTerminatedException("Language server stdout read process terminated unexpectedly") + log.error(str(exception)) + self._cancel_pending_requests(exception) - def run_forever_stderr(self) -> None: + def _read_ls_process_stderr(self) -> None: """ Continuously read from the language server process stderr and log the messages """ @@ -334,6 +368,7 @@ class SolidLanguageServerHandler: self._log("LSP stderr: " + line.decode(ENCODING, errors="replace")) except (BrokenPipeError, ConnectionResetError): pass + log.info("Language server stderr reader thread has terminated") def _handle_body(self, body: bytes) -> None: """ @@ -386,30 +421,40 @@ class SolidLanguageServerHandler: # Use lock to prevent race conditions on tasks and task_counter self._send_payload(make_error_response(request_id, err)) + def _cancel_pending_requests(self, exception: Exception) -> None: + """ + Cancel all pending requests by setting their results to an error + """ + with self._response_handlers_lock: + log.info("Cancelling %d pending language server requests", len(self._pending_requests)) + for request in self._pending_requests.values(): + log.info("Cancelling %s", request) + request.on_error(exception) + self._pending_requests.clear() + def send_request(self, method: str, params: dict | None = None) -> PayloadLike: """ Send request to the server, register the request id, and wait for the response """ - request = Request() - - # Use lock to prevent race conditions on request_id and _response_handlers with self._request_id_lock: request_id = self.request_id self.request_id += 1 + request = Request(request_id=request_id, method=method) + log.debug("Starting: %s", request) + with self._response_handlers_lock: - self._response_handlers[request_id] = request + self._pending_requests[request_id] = request self._send_payload(make_request(method, request_id, params)) self._log(f"Waiting for response to request {method} with params:\n{params}") result = request.get_result(timeout=self._request_timeout) + log.debug("Completed: %s", request) self._log("Processing result") if result.is_error(): - raise LanguageServerException( - f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}" - ) from result.error + raise LanguageServerException(f"Error processing request {method} with params:\n{params}", cause=result.error) from result.error self._log(f"Returning non-error result, which is:\n{result.payload}") return result.payload @@ -451,7 +496,7 @@ class SolidLanguageServerHandler: Handle the response received from the server for a request, using the id to determine the request """ with self._response_handlers_lock: - request = self._response_handlers.pop(response["id"]) + request = self._pending_requests.pop(response["id"]) if "result" in response and "error" not in response: request.on_result(response["result"]) @@ -499,7 +544,7 @@ class SolidLanguageServerHandler: except asyncio.CancelledError: return except Exception as ex: - if (not self._received_shutdown) and self.logger: + if (not self._is_shutting_down) and self.logger: self.logger( "client", "logger", diff --git a/src/solidlsp/ls_request.py b/src/solidlsp/ls_request.py index f81727d4..cacd2872 100644 --- a/src/solidlsp/ls_request.py +++ b/src/solidlsp/ls_request.py @@ -1,11 +1,13 @@ -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union -from solidlsp.ls_handler import SolidLanguageServerHandler from solidlsp.lsp_protocol_handler import lsp_types +if TYPE_CHECKING: + from .ls_handler import SolidLanguageServerHandler + class LanguageServerRequest: - def __init__(self, handler: SolidLanguageServerHandler): + def __init__(self, handler: "SolidLanguageServerHandler"): self.handler = handler def _send_request(self, method: str, params: Any | None = None) -> Any: From 9368ce446cd41481f686569a41bed6ed49dffedd Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 23:07:54 +0200 Subject: [PATCH 18/19] Rename Error -> LSPError --- src/solidlsp/ls.py | 4 ++-- src/solidlsp/ls_handler.py | 14 +++++++------- src/solidlsp/lsp_protocol_handler/server.py | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 7cb82d72..a2a42880 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -30,7 +30,7 @@ from solidlsp.lsp_protocol_handler import lsp_types as LSPTypes from solidlsp.lsp_protocol_handler.lsp_constants import LSPConstants from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind from solidlsp.lsp_protocol_handler.server import ( - Error, + LSPError, ProcessLaunchInfo, StringDict, ) @@ -672,7 +672,7 @@ class SolidLanguageServer(ABC): response = self._send_references_request(relative_file_path, line=line, column=column) except Exception as e: # Catch LSP internal error (-32603) and raise a more informative exception - if isinstance(e, Error) and getattr(e, "code", None) == -32603: + if isinstance(e, LSPError) and getattr(e, "code", None) == -32603: raise RuntimeError( f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. " "This often occurs when requesting references for a symbol not referenced in the expected way. " diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index ab8a6772..eef94cfe 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -20,7 +20,7 @@ from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes from solidlsp.lsp_protocol_handler.server import ( ENCODING, - Error, + LSPError, MessageType, PayloadLike, ProcessLaunchInfo, @@ -414,7 +414,7 @@ class SolidLanguageServerHandler: """ self._send_payload(make_response(request_id, params)) - def send_error_response(self, request_id: Any, err: Error) -> None: + def send_error_response(self, request_id: Any, err: LSPError) -> None: """ Send error response to the given request id to the server with the given error """ @@ -501,9 +501,9 @@ class SolidLanguageServerHandler: if "result" in response and "error" not in response: request.on_result(response["result"]) elif "result" not in response and "error" in response: - request.on_error(Error.from_lsp(response["error"])) + request.on_error(LSPError.from_lsp(response["error"])) else: - request.on_error(Error(ErrorCodes.InvalidRequest, "")) + request.on_error(LSPError(ErrorCodes.InvalidRequest, "")) def _request_handler(self, response: StringDict) -> None: """ @@ -516,7 +516,7 @@ class SolidLanguageServerHandler: if not handler: self.send_error_response( request_id, - Error( + LSPError( ErrorCodes.MethodNotFound, f"method '{method}' not handled on client.", ), @@ -524,10 +524,10 @@ class SolidLanguageServerHandler: return try: self.send_response(request_id, handler(params)) - except Error as ex: + except LSPError as ex: self.send_error_response(request_id, ex) except Exception as ex: - self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex))) + self.send_error_response(request_id, LSPError(ErrorCodes.InternalError, str(ex))) def _notification_handler(self, response: StringDict) -> None: """ diff --git a/src/solidlsp/lsp_protocol_handler/server.py b/src/solidlsp/lsp_protocol_handler/server.py index 0cb59490..62d50e7f 100644 --- a/src/solidlsp/lsp_protocol_handler/server.py +++ b/src/solidlsp/lsp_protocol_handler/server.py @@ -59,7 +59,7 @@ class ProcessLaunchInfo: cwd: str = os.getcwd() -class Error(Exception): +class LSPError(Exception): def __init__(self, code: ErrorCodes, message: str) -> None: super().__init__(message) self.code = code @@ -68,8 +68,8 @@ class Error(Exception): return {"code": self.code, "message": super().__str__()} @classmethod - def from_lsp(cls, d: StringDict) -> "Error": - return Error(d["code"], d["message"]) + def from_lsp(cls, d: StringDict) -> "LSPError": + return LSPError(d["code"], d["message"]) def __str__(self) -> str: return f"{super().__str__()} ({self.code})" @@ -79,7 +79,7 @@ def make_response(request_id: Any, params: PayloadLike) -> StringDict: return {"jsonrpc": "2.0", "id": request_id, "result": params} -def make_error_response(request_id: Any, err: Error) -> StringDict: +def make_error_response(request_id: Any, err: LSPError) -> StringDict: return {"jsonrpc": "2.0", "id": request_id, "error": err.to_lsp()} From bb0e944b6ddd5342c20c481f9cffabce92a01e75 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Sun, 27 Jul 2025 23:09:17 +0200 Subject: [PATCH 19/19] Rename LanguageServerException -> SolidLSPException (making clear that the exception originates from the proxy framework of SolidLSP) --- src/serena/tools/tools_base.py | 4 ++-- .../csharp_language_server.py | 22 +++++++++---------- src/solidlsp/language_servers/omnisharp.py | 4 ++-- src/solidlsp/ls.py | 20 ++++++++--------- src/solidlsp/ls_exceptions.py | 2 +- src/solidlsp/ls_handler.py | 4 ++-- src/solidlsp/ls_utils.py | 22 +++++++++---------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index c8163390..cd025970 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -15,7 +15,7 @@ from serena.prompt_factory import PromptFactory from serena.symbol import LanguageServerSymbolRetriever from serena.util.class_decorators import singleton from serena.util.inspection import iter_subclasses -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException if TYPE_CHECKING: from serena.agent import LinesRead, MemoriesManager, SerenaAgent @@ -239,7 +239,7 @@ class Tool(Component): # apply the actual tool try: result = apply_fn(**kwargs) - except LanguageServerException as e: + except SolidLSPException as e: if e.is_language_server_terminated(): log.error(f"Language server terminated while executing tool ({e}). Restarting the language server and retrying ...") self.agent.reset_language_server() diff --git a/src/solidlsp/language_servers/csharp_language_server.py b/src/solidlsp/language_servers/csharp_language_server.py index 7d11a1df..cd1b83ad 100644 --- a/src/solidlsp/language_servers/csharp_language_server.py +++ b/src/solidlsp/language_servers/csharp_language_server.py @@ -20,7 +20,7 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import PathUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -240,7 +240,7 @@ class CSharpLanguageServer(SolidLanguageServer): elif system == "linux": return "linux-x64" if machine in ["x86_64", "amd64"] else "linux-arm64" else: - raise LanguageServerException(f"Unsupported platform: {system} {machine}") + raise SolidLSPException(f"Unsupported platform: {system} {machine}") @staticmethod def _get_runtime_dependencies(runtime_id: str) -> tuple[RuntimeDependency, RuntimeDependency]: @@ -255,9 +255,9 @@ class CSharpLanguageServer(SolidLanguageServer): dotnet_runtime_dep = dep if not lang_server_dep: - raise LanguageServerException(f"No C# language server dependency found for platform {runtime_id}") + raise SolidLSPException(f"No C# language server dependency found for platform {runtime_id}") if not dotnet_runtime_dep: - raise LanguageServerException(f"No .NET runtime dependency found for platform {runtime_id}") + raise SolidLSPException(f"No .NET runtime dependency found for platform {runtime_id}") return lang_server_dep, dotnet_runtime_dep @@ -300,7 +300,7 @@ class CSharpLanguageServer(SolidLanguageServer): CSharpLanguageServer._extract_language_server(lang_server_dep, package_path, server_dir) if not server_dll.exists(): - raise LanguageServerException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction") + raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction") # Make executable on Unix systems if platform.system().lower() != "windows": @@ -326,7 +326,7 @@ class CSharpLanguageServer(SolidLanguageServer): source_dir = possible_dir break else: - raise LanguageServerException(f"Could not find language server files in package. Searched in {package_path}") + raise SolidLSPException(f"Could not find language server files in package. Searched in {package_path}") # Copy files to cache directory server_dir.mkdir(parents=True, exist_ok=True) @@ -358,7 +358,7 @@ class CSharpLanguageServer(SolidLanguageServer): break if not package_base_address: - raise LanguageServerException("Could not find package base address in Azure NuGet feed") + raise SolidLSPException("Could not find package base address in Azure NuGet feed") # Construct the download URL for the specific package package_id_lower = package_name.lower() @@ -385,7 +385,7 @@ class CSharpLanguageServer(SolidLanguageServer): return package_extract_dir except Exception as e: - raise LanguageServerException( + raise SolidLSPException( f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}" ) from e @@ -448,7 +448,7 @@ class CSharpLanguageServer(SolidLanguageServer): return str(dotnet_exe) except Exception as e: - raise LanguageServerException(f"Failed to download .NET 9 runtime from {url}: {e}") from e + raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e def _get_initialize_params(self) -> InitializeParams: """ @@ -623,7 +623,7 @@ class CSharpLanguageServer(SolidLanguageServer): self.server.start() except Exception as e: self.logger.log(f"Failed to start language server process: {e}", logging.ERROR) - raise LanguageServerException(f"Failed to start C# language server: {e}") + raise SolidLSPException(f"Failed to start C# language server: {e}") # Send initialization initialize_params = self._get_initialize_params() @@ -633,7 +633,7 @@ class CSharpLanguageServer(SolidLanguageServer): init_response = self.server.send.initialize(initialize_params) self.logger.log(f"Received initialize response: {init_response}", logging.DEBUG) except Exception as e: - raise LanguageServerException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e + raise SolidLSPException(f"Failed to initialize C# language server for {self.repository_root_path}: {e}") from e # Apply diagnostic capabilities self._force_pull_diagnostics(init_response) diff --git a/src/solidlsp/language_servers/omnisharp.py b/src/solidlsp/language_servers/omnisharp.py index eafaa310..492eea32 100644 --- a/src/solidlsp/language_servers/omnisharp.py +++ b/src/solidlsp/language_servers/omnisharp.py @@ -14,7 +14,7 @@ from overrides import override from solidlsp.ls import SolidLanguageServer from solidlsp.ls_config import LanguageServerConfig -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams @@ -68,7 +68,7 @@ class OmniSharp(SolidLanguageServer): slnfilename = find_least_depth_sln_file(repository_root_path) if slnfilename is None: logger.log("No *.sln file found in repository", logging.ERROR) - raise LanguageServerException("No SLN file found in repository") + raise SolidLSPException("No SLN file found in repository") cmd = " ".join( [ diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index a2a42880..2a8bc1ef 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -21,7 +21,7 @@ from serena.text_utils import MatchedConsecutiveLines from serena.util.file_system import match_path from solidlsp import ls_types from solidlsp.ls_config import Language, LanguageServerConfig -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_handler import SolidLanguageServerHandler from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_utils import FileUtils, PathUtils, TextUtils @@ -209,7 +209,7 @@ class SolidLanguageServer(ABC): else: logger.log(f"Language {config.code_language} is not supported", logging.ERROR) - raise LanguageServerException(f"Language {config.code_language} is not supported") + raise SolidLSPException(f"Language {config.code_language} is not supported") ls.set_request_timeout(timeout) return ls @@ -427,7 +427,7 @@ class SolidLanguageServer(ABC): "open_file called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -483,7 +483,7 @@ class SolidLanguageServer(ABC): "insert_text_at_position called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -529,7 +529,7 @@ class SolidLanguageServer(ABC): "insert_text_at_position called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) uri = pathlib.Path(absolute_file_path).as_uri() @@ -573,7 +573,7 @@ class SolidLanguageServer(ABC): "request_definition called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") with self.open_file(relative_file_path): # sending request to the language server and waiting for response @@ -665,7 +665,7 @@ class SolidLanguageServer(ABC): "request_references called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") with self.open_file(relative_file_path): try: @@ -724,7 +724,7 @@ class SolidLanguageServer(ABC): "request_text_document_diagnostics called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") with self.open_file(relative_file_path): response = self.server.send.text_document_diagnostic( @@ -1286,7 +1286,7 @@ class SolidLanguageServer(ABC): "request_referencing_symbols called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") # First, get all references to the symbol references = self.request_references(relative_file_path, line, column) @@ -1558,7 +1558,7 @@ class SolidLanguageServer(ABC): "request_defining_symbol called before Language Server started", logging.ERROR, ) - raise LanguageServerException("Language Server not started") + raise SolidLSPException("Language Server not started") # Get the definition location(s) definitions = self.request_definition(relative_file_path, line, column) diff --git a/src/solidlsp/ls_exceptions.py b/src/solidlsp/ls_exceptions.py index a3bb0f11..7c03cfe5 100644 --- a/src/solidlsp/ls_exceptions.py +++ b/src/solidlsp/ls_exceptions.py @@ -3,7 +3,7 @@ This module contains the exceptions raised by the framework. """ -class LanguageServerException(Exception): +class SolidLSPException(Exception): def __init__(self, message: str, cause: Exception | None = None): """ Initializes the exception with the given message. diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py index eef94cfe..4565d2c5 100644 --- a/src/solidlsp/ls_handler.py +++ b/src/solidlsp/ls_handler.py @@ -14,7 +14,7 @@ from typing import Any import psutil from sensai.util.string import ToStringMixin -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_request import LanguageServerRequest from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification from solidlsp.lsp_protocol_handler.lsp_types import ErrorCodes @@ -454,7 +454,7 @@ class SolidLanguageServerHandler: self._log("Processing result") if result.is_error(): - raise LanguageServerException(f"Error processing request {method} with params:\n{params}", cause=result.error) from result.error + raise SolidLSPException(f"Error processing request {method} with params:\n{params}", cause=result.error) from result.error self._log(f"Returning non-error result, which is:\n{result.payload}") return result.payload diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index d1161880..6daa3e7f 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -14,7 +14,7 @@ from pathlib import Path, PurePath import requests -from solidlsp.ls_exceptions import LanguageServerException +from solidlsp.ls_exceptions import SolidLSPException from solidlsp.ls_logger import LanguageServerLogger from solidlsp.ls_types import UnifiedSymbolInformation @@ -170,13 +170,13 @@ class FileUtils: """ if not os.path.exists(file_path): logger.log(f"File read '{file_path}' failed: File does not exist.", logging.ERROR) - raise LanguageServerException(f"File read '{file_path}' failed: File does not exist.") + raise SolidLSPException(f"File read '{file_path}' failed: File does not exist.") try: with open(file_path, encoding="utf-8") as inp_file: return inp_file.read() except Exception as exc: logger.log(f"File read '{file_path}' failed to read with encoding 'utf-8': {exc}", logging.ERROR) - raise LanguageServerException("File read failed.") from None + raise SolidLSPException("File read failed.") from None @staticmethod def download_file(logger: LanguageServerLogger, url: str, target_path: str) -> None: @@ -187,12 +187,12 @@ class FileUtils: response = requests.get(url, stream=True, timeout=60) if response.status_code != 200: logger.log(f"Error downloading file '{url}': {response.status_code} {response.text}", logging.ERROR) - raise LanguageServerException("Error downloading file.") + raise SolidLSPException("Error downloading file.") with open(target_path, "wb") as f: shutil.copyfileobj(response.raw, f) except Exception as exc: logger.log(f"Error downloading file '{url}': {exc}", logging.ERROR) - raise LanguageServerException("Error downloading file.") from None + raise SolidLSPException("Error downloading file.") from None @staticmethod def download_and_extract_archive(logger: LanguageServerLogger, url: str, target_path: str, archive_type: str) -> None: @@ -220,10 +220,10 @@ class FileUtils: shutil.copyfileobj(f_in, f_out) else: logger.log(f"Unknown archive type '{archive_type}' for extraction", logging.ERROR) - raise LanguageServerException(f"Unknown archive type '{archive_type}'") + raise SolidLSPException(f"Unknown archive type '{archive_type}'") except Exception as exc: logger.log(f"Error extracting archive '{tmp_file_name}' obtained from '{url}': {exc}", logging.ERROR) - raise LanguageServerException("Error extracting archive.") from exc + raise SolidLSPException("Error extracting archive.") from exc finally: for tmp_file_name in tmp_files: if os.path.exists(tmp_file_name): @@ -289,7 +289,7 @@ class PlatformUtils: platform_id += "-" + libc return PlatformId(platform_id) else: - raise LanguageServerException(f"Unknown platform: {system=}, {machine=}, {bitness=}") + raise SolidLSPException(f"Unknown platform: {system=}, {machine=}, {bitness=}") @staticmethod def _determine_windows_machine_type(): @@ -345,7 +345,7 @@ class PlatformUtils: available_version_cmd_output.append(version_cmd_output) if not available_version_cmd_output: - raise LanguageServerException("dotnet not found on the system") + raise SolidLSPException("dotnet not found on the system") # Check for supported versions in order of preference (latest first) for version_cmd_output in available_version_cmd_output: @@ -361,7 +361,7 @@ class PlatformUtils: return DotnetVersion.V4 # If no supported version found, raise exception with all available versions - raise LanguageServerException( + raise SolidLSPException( f"No supported dotnet version found. Available versions: {', '.join(available_version_cmd_output)}. Supported versions: 4, 6, 7, 8" ) except (FileNotFoundError, subprocess.CalledProcessError): @@ -369,7 +369,7 @@ class PlatformUtils: result = subprocess.run(["mono", "--version"], capture_output=True, check=True) return DotnetVersion.VMONO except (FileNotFoundError, subprocess.CalledProcessError): - raise LanguageServerException("dotnet or mono not found on the system") + raise SolidLSPException("dotnet or mono not found on the system") class SymbolUtils: