mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 02:14:49 +00:00
Merge branch 'main' into mypy_on_solidlsp
Conflicts: src/solidlsp/ls.py src/solidlsp/ls_handler.py src/solidlsp/ls_request.py
This commit is contained in:
commit
8e8f304377
21 files changed
+452
-211
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+37
-5
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -141,15 +142,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(
|
||||
@@ -237,6 +245,7 @@ class ModeCommands(AutoRegisteringGroup):
|
||||
@click.command("create", help="Create a new mode or copy an internal one.")
|
||||
@click.option(
|
||||
"--name",
|
||||
"-n",
|
||||
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.",
|
||||
@@ -258,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
|
||||
@@ -313,6 +322,7 @@ class ContextCommands(AutoRegisteringGroup):
|
||||
@click.command("create", help="Create a new context or copy an internal one.")
|
||||
@click.option(
|
||||
"--name",
|
||||
"-n",
|
||||
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",
|
||||
@@ -334,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
|
||||
@@ -445,11 +455,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 <command> --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", 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 +489,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)
|
||||
|
||||
|
||||
|
||||
@@ -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) -> 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) -> 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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 SolidLSPException
|
||||
|
||||
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 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()
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
@@ -76,8 +77,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
|
||||
@@ -182,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:
|
||||
@@ -197,11 +196,13 @@ class GitignoreParser:
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Determine if pattern is anchored to the gitignore directory
|
||||
is_anchored = "/" in line[:-1] or line.startswith("/")
|
||||
# Handle escaped characters at the beginning
|
||||
if line.startswith(("\\#", "\\!")):
|
||||
line = line[1:]
|
||||
|
||||
# Remove leading slash for processing
|
||||
if line.startswith("/"):
|
||||
# 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:]
|
||||
|
||||
# Adjust pattern based on gitignore file location
|
||||
@@ -256,6 +257,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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,27 +72,29 @@ 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_process = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
check=True,
|
||||
cwd=cwd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
**kwargs,
|
||||
)
|
||||
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
|
||||
|
||||
@@ -21,7 +21,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, InitializeResult
|
||||
@@ -247,7 +247,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]:
|
||||
@@ -262,9 +262,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
|
||||
|
||||
@@ -314,7 +314,7 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
cls._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":
|
||||
@@ -340,7 +340,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)
|
||||
@@ -374,7 +374,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()
|
||||
@@ -401,7 +401,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
|
||||
|
||||
@@ -468,7 +468,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:
|
||||
"""
|
||||
@@ -643,7 +643,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()
|
||||
@@ -653,7 +653,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)
|
||||
|
||||
@@ -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
|
||||
@@ -71,7 +71,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(
|
||||
[
|
||||
|
||||
@@ -11,6 +11,7 @@ from time import sleep
|
||||
from typing import Any, cast
|
||||
|
||||
from overrides import override
|
||||
from sensai.util.logging import LogTime
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
@@ -125,7 +126,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(
|
||||
|
||||
+53
-11
@@ -22,7 +22,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
|
||||
@@ -31,7 +31,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,
|
||||
)
|
||||
@@ -228,7 +228,7 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
else:
|
||||
logger.log(f"Language {config.code_language} is not supported", logging.ERROR) # type: ignore
|
||||
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
|
||||
@@ -451,7 +451,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()
|
||||
@@ -507,7 +507,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()
|
||||
@@ -553,7 +553,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()
|
||||
@@ -597,7 +597,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
|
||||
@@ -689,14 +689,14 @@ 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:
|
||||
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. "
|
||||
@@ -734,6 +734,48 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
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 SolidLSPException("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:
|
||||
"""
|
||||
Retrieve the full content of the given file.
|
||||
@@ -1268,7 +1310,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)
|
||||
@@ -1542,7 +1584,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)
|
||||
|
||||
@@ -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):
|
||||
class SolidLSPException(Exception):
|
||||
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
|
||||
+87
-65
@@ -12,14 +12,15 @@ 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_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
|
||||
from solidlsp.lsp_protocol_handler.server import (
|
||||
ENCODING,
|
||||
Error,
|
||||
LSPError,
|
||||
MessageType,
|
||||
PayloadLike,
|
||||
ProcessLaunchInfo,
|
||||
@@ -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[Request.Result] = 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.
|
||||
@@ -104,15 +130,15 @@ class SolidLanguageServerHandler:
|
||||
start_independent_lsp_process: bool = 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
|
||||
self.process: subprocess.Popen[bytes] | None = 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: dict[str, Callable[[Any], Any]] = {}
|
||||
self.on_notification_handlers: dict[str, Callable[[Any], None]] = {}
|
||||
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()
|
||||
@@ -225,21 +251,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: subprocess.Popen[bytes], terminate: bool = True) -> None:
|
||||
"""Send signal (terminate or kill) to the process and all its children."""
|
||||
signal_method = "terminate" if terminate else "kill"
|
||||
@@ -276,21 +287,13 @@ 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")
|
||||
# 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:
|
||||
"""
|
||||
@@ -302,27 +305,26 @@ class SolidLanguageServerHandler:
|
||||
@staticmethod
|
||||
def _read_bytes_from_process(process: subprocess.Popen[bytes], stream: Any, num_bytes: int) -> 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()
|
||||
@@ -341,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
|
||||
"""
|
||||
@@ -357,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:
|
||||
"""
|
||||
@@ -402,37 +414,47 @@ 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
|
||||
"""
|
||||
# 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 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
|
||||
@@ -474,14 +496,14 @@ 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"])
|
||||
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:
|
||||
"""
|
||||
@@ -494,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.",
|
||||
),
|
||||
@@ -502,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:
|
||||
"""
|
||||
@@ -522,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",
|
||||
|
||||
+60
-53
@@ -1,12 +1,19 @@
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Union
|
||||
|
||||
from solidlsp.lsp_protocol_handler import lsp_types
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .ls_handler import SolidLanguageServerHandler
|
||||
|
||||
|
||||
class LanguageServerRequest:
|
||||
def __init__(self, send_request: Callable[[str, Any], Any]) -> None:
|
||||
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
|
||||
@@ -14,7 +21,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
|
||||
@@ -24,7 +31,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
|
||||
@@ -32,7 +39,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
|
||||
@@ -40,7 +47,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
|
||||
@@ -48,7 +55,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
|
||||
@@ -57,7 +64,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
|
||||
@@ -65,7 +72,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.
|
||||
@@ -73,42 +80,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
|
||||
@@ -116,7 +123,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
|
||||
@@ -124,7 +131,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
|
||||
@@ -132,14 +139,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.
|
||||
@@ -147,21 +154,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
|
||||
@@ -170,7 +177,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
|
||||
@@ -179,7 +186,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.
|
||||
@@ -188,21 +195,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.
|
||||
@@ -211,7 +218,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.
|
||||
@@ -219,7 +226,7 @@ class LanguageServerRequest:
|
||||
server. The only notification that is sent after a shutdown request
|
||||
is the exit event.
|
||||
"""
|
||||
return self.send_request("shutdown", None)
|
||||
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
|
||||
@@ -229,7 +236,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
|
||||
@@ -242,24 +249,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
|
||||
@@ -268,7 +275,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
|
||||
@@ -276,7 +283,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
|
||||
@@ -284,7 +291,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
|
||||
@@ -294,18 +301,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
|
||||
@@ -319,7 +326,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
|
||||
@@ -327,52 +334,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)
|
||||
@@ -317,3 +317,27 @@ 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. """
|
||||
+14
-11
@@ -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
|
||||
|
||||
@@ -175,13 +175,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:
|
||||
@@ -193,12 +193,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:
|
||||
@@ -226,10 +226,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):
|
||||
@@ -253,6 +253,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):
|
||||
"""
|
||||
@@ -292,7 +295,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() -> str:
|
||||
@@ -348,7 +351,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:
|
||||
@@ -364,7 +367,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):
|
||||
@@ -372,7 +375,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:
|
||||
|
||||
@@ -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"
|
||||
@@ -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()}
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user