diff --git a/.gitignore b/.gitignore index ce9c54f5..bee98b92 100644 --- a/.gitignore +++ b/.gitignore @@ -202,6 +202,7 @@ pylint.html /agent-ui # dynamic LS installations /src/multilspy/language_servers/*/static +/src/solidlsp/language_servers/*/static # Cache is in .multilspy **/.multilspy/** diff --git a/.serena/memories/solid-lsp-architecture.md b/.serena/memories/solid-lsp-architecture.md new file mode 100644 index 00000000..4a334cf2 --- /dev/null +++ b/.serena/memories/solid-lsp-architecture.md @@ -0,0 +1,161 @@ +# Solid-LSP: Deadlock-Free Language Server Architecture + +## Overview + +Solid-LSP (`src/solidlsp/`) is a **simplified, deadlock-resistant** reimplementation of the language server communication layer, designed to replace the problematic `multilspy` architecture that suffered from asyncio contamination issues when used with MCP servers. + +## The Problem Solid-LSP Solves + +### Root Cause: Asyncio Context Contamination +The original `multilspy` implementation created **asyncio deadlocks** in MCP environments due to: + +1. **Dual Event Loop Interference**: MCP server runs its own asyncio event loop, while SerenaAgent creates separate asyncio loops for language servers and dashboard +2. **Coroutine Leakage**: When MCP server calls SerenaAgent tools, coroutines created by `multilspy.LanguageServer.request_full_symbol_tree()` were leaked into the MCP server's asyncio context but never awaited +3. **Resource Exhaustion**: Accumulated unawaited coroutines in the MCP server's event loop eventually caused blocking and apparent "hangs" +4. **MCP-Specific Issue**: Only occurred when using MCP clients, not in direct script execution + +### Evidence from Logs +``` +INFO mcp.server.lowlevel.server:_handle_message:524 - Warning: RuntimeWarning: coroutine 'LanguageServer.request_full_symbol_tree' was never awaited +``` + +## Solid-LSP Architecture + +### Core Design Principles + +1. **Simplified Protocol Handling**: Eliminates the complex `lsp_protocol_handler` layer from `multilspy` +2. **Direct Async Management**: More explicit control over async operations without complex threading abstractions +3. **Single Process Operation**: Designed to work directly in the MCP server process without requiring process isolation +4. **Clean Asyncio Boundaries**: Prevents coroutine leakage between async contexts + +### Key Architectural Differences from multilspy + +#### 1. Simplified Handler Architecture +- **multilspy**: Complex `LanguageServerHandler` with extensive async task management and event loops +- **solid-lsp**: Streamlined `SolidLanguageServerHandler` with direct process management + +#### 2. Protocol Simplification +- **No separate protocol handler package**: Direct LSP communication without abstraction layers +- **Reduced complexity**: Fewer components and dependencies +- **Same language server support**: Identical `language_servers/` implementations for all supported languages + +#### 3. Enhanced Process Control +**solid-lsp** provides additional methods for finer process control: +- `_start_server_process()`: Direct process control +- `_start_server()`: Server initialization +- `_server_context` attribute: Enhanced server state management + +## Process Isolation: No Longer Required + +### Key Benefit: Single Process Operation + +With solid-lsp, **process isolation is no longer needed** and is **disabled by default**: + +```python +# src/serena/mcp.py:581-585 +if USE_SOLID_LSP: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) +else: + # using multilspy requires process isolation to prevent asyncio contamination + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) +``` + +### Benefits of Single Process Operation +1. **Lower Resource Usage**: No separate processes for SerenaAgent and language servers +2. **Reduced Latency**: Direct method calls instead of inter-process communication +3. **Simplified Architecture**: No IPC overhead or process management complexity +4. **Better Performance**: Eliminated serialization/deserialization overhead +5. **Easier Debugging**: All components in same process, simplified stack traces + +### Why multilspy Required Process Isolation +- **Asyncio Contamination**: multilspy's async patterns leaked coroutines into MCP server context +- **Mandatory Isolation**: Process boundaries were the only way to prevent deadlocks +- **Performance Penalty**: IPC overhead was necessary for stability + +### How solid-lsp Enables Single Process +- **Clean Async Patterns**: No coroutine leakage between MCP server and language server contexts +- **Direct Integration**: Can safely run in same process as MCP server +- **Stable Operation**: No more timeout issues or hanging operations + +## Supported Language Servers + +Solid-LSP maintains compatibility with all language servers from `multilspy`: + +### Primary Languages +- **Python**: Pyright Language Server (`src/solidlsp/language_servers/pyright_language_server/`) +- **Java**: Eclipse JDTLS (`src/solidlsp/language_servers/eclipse_jdtls/`) +- **TypeScript/JavaScript**: TypeScript Language Server (`src/solidlsp/language_servers/typescript_language_server/`) + +### Additional Languages +- **C#**: OmniSharp (`src/solidlsp/language_servers/omnisharp/`) +- **Rust**: Rust-Analyzer (`src/solidlsp/language_servers/rust_analyzer/`) +- **Go**: Gopls (`src/solidlsp/language_servers/gopls/`) +- **Ruby**: Solargraph (`src/solidlsp/language_servers/solargraph/`) +- **C++**: Clangd (`src/solidlsp/language_servers/clangd_language_server/`) +- **Dart**: Dart Language Server (`src/solidlsp/language_servers/dart_language_server/`) +- **PHP**: Intelephense (`src/solidlsp/language_servers/intelephense/`) +- **Kotlin**: Kotlin Language Server (`src/solidlsp/language_servers/kotlin_language_server/`) + +## Implementation Details + +### Core Classes + +#### `SolidLanguageServer` (`src/solidlsp/ls.py`) +- **1,634 lines**: Main language server interface +- **Enhanced methods**: Same semantic capabilities as `multilspy.LanguageServer` but with improved async handling +- **Lifecycle control**: `start()`, `stop()`, `is_running()`, `language_server()` property (similar to multilspy's `SyncLanguageServer`) + +#### `SolidLanguageServerHandler` (`src/solidlsp/ls_handler.py`) +- **512 lines**: Simplified LSP protocol handler +- **Direct process management**: More straightforward than `multilspy`'s complex async task orchestration +- **Improved cleanup**: Better resource management and process termination + +#### `SolidLspRequest` (`src/solidlsp/lsp_request.py`) +- **Simplified request handling**: Direct LSP request/response without complex protocol abstractions + +### Configuration Integration + +Solid-LSP is controlled by the `USE_SOLID_LSP` constant in `src/serena/constants.py`: + +```python +USE_SOLID_LSP = True # Default: enabled +``` + +This allows: +- **Default Operation**: solid-lsp enabled by default for stability +- **Fallback Capability**: Can switch back to multilspy + process isolation if needed +- **A/B Testing**: Easy comparison between implementations + +## Deadlock Prevention + +### How Solid-LSP Prevents Deadlocks + +1. **Simplified Async Patterns**: Reduces complexity that led to coroutine leakage +2. **Clean Boundaries**: Proper async context management prevents MCP server contamination +3. **Direct Operations**: Eliminates abstraction layers where async context could be corrupted +4. **Single Process Safety**: Designed to work safely within MCP server process + +### Verification + +The solid-lsp approach provides: +- **No more timeout issues**: Clean async boundaries prevent MCP server contamination +- **Stable MCP operation**: Language server calls no longer hang after first timeout +- **Resource efficiency**: Lower memory usage without separate processes +- **Reliable semantic tools**: `find_symbol` and other tools work consistently +- **Better Performance**: Direct method calls instead of IPC + +## Migration Strategy + +### Current Status +- **Default**: Solid-LSP is the default implementation (`USE_SOLID_LSP = True`) +- **Stable**: Resolves known deadlock issues in MCP environments +- **Compatible**: Supports all existing language servers and semantic operations +- **Performant**: Single process operation with lower overhead + +### Rollback Path +If issues arise, can fallback to multilspy with process isolation: +1. **Disable solid-lsp**: Set `USE_SOLID_LSP = False` +2. **Automatic fallback**: System automatically uses multilspy + process isolation +3. **Performance trade-off**: Higher resource usage but guaranteed isolation + +Solid-LSP represents an **architectural evolution** that eliminates the need for process isolation while maintaining all semantic capabilities and providing **better performance and stability** for MCP server deployments. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b8f786..51a8fc82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. +* **Reduce the use of asyncio to a minimum**, improving stability and reducing the need for workarounds + * Switch to newly developed fully synchronous LSP library `solidlsp` (derived from `multilspy`) + * Switch from fastapi (which uses asyncio) to Flask in the Serena dashboard + * The MCP server is the only asynchronous component now, which resolves cross-component loop contamination, + such that process isolation is no longer required. + Neither are non-graceful shutdowns on Windows. + +Fixes: +* Fix `ExecuteShellCommandTool` and `GetCurrentConfigTool` hanging on Windows +* Fix project activation by name via `--project` not working (was broken in previous release) + # 2025-06-20 * **Overhaul and major improvement of editing tools!** diff --git a/pyproject.toml b/pyproject.toml index 467632ab..9740f792 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "overrides>=7.7.0,<8", "python-dotenv>=1.0.0, <2", "mcp>=1.5.0", - "fastapi>=0.115.12", + "flask>=3.0.0", "sensai-utils>=1.4.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", @@ -112,10 +112,10 @@ PYDEVD_DISABLE_FILE_VALIDATION = "1" # For custom markers, one can either adjust the env var or just use -m option in the command line, # as the second -m option will override the first one. test = "pytest test -vv -m \"${PYTEST_MARKERS:-not java and not rust and not isolated_process}\"" -_black_check = "black --check --exclude src/multilspy/ src scripts test" -_ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ src scripts test" -_black_format = "black --exclude .venv/|src/multilspy/ src scripts test" -_ruff_format = "ruff check --exclude src/multilspy/ --fix src scripts test" +_black_check = "black --check --exclude src/multilspy/ --exclude src/solidlsp/ src scripts test" +_ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ --exclude src/solidlsp/ src scripts test" +_black_format = "black --exclude .venv/|src/multilspy/|src/solidlsp/ src scripts test" +_ruff_format = "ruff check --exclude src/multilspy/ --exclude src/solidlsp/ --fix src scripts test" lint = [ "_black_check", "_ruff_check", diff --git a/src/serena/__init__.py b/src/serena/__init__.py index 7b7f7b9a..1643d835 100644 --- a/src/serena/__init__.py +++ b/src/serena/__init__.py @@ -1,20 +1,23 @@ __version__ = "2025-06-21" +import logging + +log = logging.getLogger(__name__) + def serena_version() -> str: """ :return: the version of the package, including git status if available. """ + from serena.util.git import get_git_status + version = __version__ try: - from sensai.util.git import git_status - from sensai.util.logging import LoggingDisabledContext - - with LoggingDisabledContext(): - git_status = git_status() - version += f"-{git_status.commit[:8]}" - if not git_status.is_clean: - version += "-dirty" + git_status = get_git_status() + if git_status is not None: + version += f"-{git_status.commit[:8]}" + if not git_status.is_clean: + version += "-dirty" except: pass return version diff --git a/src/serena/agent.py b/src/serena/agent.py index 0f7aefd2..ef6e2264 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -39,7 +39,14 @@ from multilspy.multilspy_logger import MultilspyLogger from multilspy.multilspy_types import SymbolKind from serena import serena_version from serena.config import SerenaAgentContext, SerenaAgentMode -from serena.constants import DEFAULT_ENCODING, PROJECT_TEMPLATE_FILE, REPO_ROOT, SELENA_CONFIG_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME +from serena.constants import ( + DEFAULT_ENCODING, + PROJECT_TEMPLATE_FILE, + REPO_ROOT, + SELENA_CONFIG_TEMPLATE_FILE, + SERENA_MANAGED_DIR_NAME, + USE_SOLID_LSP, +) from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI from serena.prompt_factory import PromptFactory, SerenaPromptFactory from serena.symbol import SymbolManager @@ -49,6 +56,7 @@ from serena.util.general import load_yaml, save_yaml from serena.util.inspection import determine_programming_language_composition, iter_subclasses from serena.util.shell import execute_shell_command from serena.util.thread import ExecutionResult, execute_with_timeout +from solidlsp.ls import SolidLanguageServer if TYPE_CHECKING: from serena.gui_log_viewer import GuiLogViewerHandler @@ -214,6 +222,10 @@ class ProjectConfig(ToStringMixin): return set(ToolRegistry.get_tool_class_by_name(tool_name) for tool_name in self.excluded_tools) +class ProjectNotFoundError(Exception): + pass + + @dataclass class Project: project_root: str @@ -666,12 +678,21 @@ def create_ls_for_project( ) ls_logger = MultilspyLogger(log_level=log_level) log.info(f"Creating language server instance for {project_instance.project_root}.") - return SyncLanguageServer.create( - multilspy_config, - ls_logger, - project_instance.project_root, - timeout=ls_timeout, - ) + if USE_SOLID_LSP: + ls = SolidLanguageServer.create( + multilspy_config, + ls_logger, + project_instance.project_root, + timeout=ls_timeout, + ) + return cast(SyncLanguageServer, ls) # TODO: Fix type + else: + return SyncLanguageServer.create( + multilspy_config, + ls_logger, + project_instance.project_root, + timeout=ls_timeout, + ) @click.command() @@ -715,11 +736,10 @@ class SerenaAgent: :param modes: list of modes in which the agent is operating (they will be combined), None for default modes. The modes may adjust prompts, tool availability, and tool descriptions. :param serena_config: the Serena configuration or None to read the configuration from the default location. - :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. - :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. - If not specified, will take the value from the serena configuration. - :param log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. - :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + :param enable_web_dashboard: whether to enable the web dashboard; If None, will take the value from the Serena configuration. + :param enable_gui_log_window: whether to enable the GUI log window; If None, will take the value from the Serena configuration. + :param log_level: the log level for the GUI log window; If None, will take the value from the serena configuration. + :param tool_timeout: the timeout in seconds for tool execution. If None, will take the value from the serena configuration. """ # obtain serena configuration using the decoupled factory function self.serena_config = create_serena_config( @@ -802,7 +822,7 @@ class SerenaAgent: if project is not None: try: self.activate_project_from_path_or_name(project) - except Exception as e: + except ProjectNotFoundError as e: log.error( f"Error activating project '{project}': {e}; Note that out-of-project configurations were migrated. " "You should now pass either --project or --project ." @@ -969,7 +989,7 @@ class SerenaAgent: log.info(f"Found registered project {project_instance.project_name} at path {project_instance.project_root}.") else: if not os.path.isdir(project_root_or_name): - raise ValueError( + raise ProjectNotFoundError( f"Project '{project_root_or_name}' not found: Not a valid project name or directory. " f"Existing project names: {self.serena_config.project_names}" ) diff --git a/src/serena/constants.py b/src/serena/constants.py index 4cbf39ab..6a277598 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -18,3 +18,9 @@ DEFAULT_MODES = ("interactive", "editing") PROJECT_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "project.template.yml") SELENA_CONFIG_TEMPLATE_FILE = str(_serena_pkg_path / "resources" / "serena_config.template.yml") + +USE_SOLID_LSP = True +"""Whether to use Solid LSP instead of multilspy for language server operations. +Multilspy uses asyncio, which creates many problems like deadlocks and requires full process +isolation (and even then some issues remain). +""" diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 22e34956..eb86e74a 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -3,10 +3,9 @@ import queue import socket import threading from collections.abc import Callable +from typing import Any -import uvicorn -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles +from flask import Flask, Response, request, send_from_directory from pydantic import BaseModel from sensai.util import logging @@ -14,6 +13,9 @@ from serena.constants import SERENA_DASHBOARD_DIR log = logging.getLogger(__name__) +# disable Werkzeug's logging to avoid cluttering the output +logging.getLogger("werkzeug").setLevel(logging.WARNING) + class MemoryLogHandler(logging.Handler): def __init__(self, level: int = logging.NOTSET) -> None: @@ -74,7 +76,7 @@ class SerenaDashboardAPI: self._memory_log_handler = memory_log_handler self._tool_names = tool_names self._shutdown_callback = shutdown_callback - self._app = FastAPI(title="Serena Dashboard") + self._app = Flask(__name__) self._setup_routes() @property @@ -82,21 +84,46 @@ class SerenaDashboardAPI: return self._memory_log_handler def _setup_routes(self) -> None: - self._app.mount("/dashboard", StaticFiles(directory=SERENA_DASHBOARD_DIR), name="dashboard") + # Static files + @self._app.route("/dashboard/") + def serve_dashboard(filename: str) -> Response: + return send_from_directory(SERENA_DASHBOARD_DIR, filename) - self._app.add_api_route("/get_log_messages", self._get_log_messages, methods=["POST"], response_model=ResponseLog) - self._app.add_api_route("/get_tool_names", self._get_tool_names, methods=["GET"], response_model=ResponseToolNames) - self._app.add_api_route("/shutdown", self._shutdown, methods=["PUT"]) + @self._app.route("/dashboard/") + def serve_dashboard_index() -> Response: + return send_from_directory(SERENA_DASHBOARD_DIR, "index.html") - async def _get_log_messages(self, request: RequestLog) -> ResponseLog: + # API routes + @self._app.route("/get_log_messages", methods=["POST"]) + def get_log_messages() -> dict[str, Any]: + request_data = request.get_json() + if not request_data: + request_log = RequestLog() + else: + request_log = RequestLog.model_validate(request_data) + + result = self._get_log_messages(request_log) + return result.model_dump() + + @self._app.route("/get_tool_names", methods=["GET"]) + def get_tool_names() -> dict[str, Any]: + result = self._get_tool_names() + return result.model_dump() + + @self._app.route("/shutdown", methods=["PUT"]) + def shutdown() -> dict[str, str]: + self._shutdown() + return {"status": "shutting down"} + + def _get_log_messages(self, request_log: RequestLog) -> ResponseLog: all_messages = self._memory_log_handler.get_log_messages() - requested_messages = all_messages[request.start_idx :] if request.start_idx <= len(all_messages) else [] + requested_messages = all_messages[request_log.start_idx :] if request_log.start_idx <= len(all_messages) else [] return ResponseLog(messages=requested_messages, max_idx=len(all_messages) - 1) - async def _get_tool_names(self) -> ResponseToolNames: + def _get_tool_names(self) -> ResponseToolNames: return ResponseToolNames(tool_names=self._tool_names) - async def _shutdown(self) -> None: + def _shutdown(self) -> None: log.info("Shutting down Serena") if self._shutdown_callback: self._shutdown_callback() @@ -125,7 +152,12 @@ class SerenaDashboardAPI: """ Runs the dashboard on the given host and port and returns the port number. """ - uvicorn.run(self._app, host=host, port=port, workers=1, log_config=None, log_level="critical") + # patch flask.cli.show_server to avoid printing the server info + from flask import cli + + cli.show_server_banner = lambda *args, **kwargs: None + + self._app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True) return port def run_in_thread(self) -> tuple[threading.Thread, int]: diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 9cbdc1f3..e2a0bc95 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -9,7 +9,8 @@ import signal import sys import threading import time -from collections.abc import AsyncIterator, Sequence +from abc import abstractmethod +from collections.abc import AsyncIterator, Iterator, Sequence from contextlib import asynccontextmanager from dataclasses import dataclass from logging import Formatter, Logger, StreamHandler @@ -23,19 +24,19 @@ from mcp.server.fastmcp.server import FastMCP, Settings from mcp.server.fastmcp.tools.base import Tool as MCPTool from pydantic_settings import SettingsConfigDict from sensai.util import logging -from sensai.util.helper import mark_used from serena.agent import ( ActivateProjectTool, Project, SerenaAgent, + SerenaConfig, ToolInterface, ToolRegistry, create_serena_config, show_fatal_exception_safe, ) from serena.config import RegisteredContext, SerenaAgentContext, SerenaAgentMode -from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES +from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES, USE_SOLID_LSP from serena.process_isolated_agent import ( ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent, @@ -66,152 +67,324 @@ class SerenaMCPRequestContext: agent: SerenaAgent -def make_tool( - tool: ToolInterface, -) -> MCPTool: - func_name = tool.get_name() - func_doc = tool.get_apply_docstring() or "" - func_arg_metadata = tool.get_apply_fn_metadata() - is_async = False - parameters = func_arg_metadata.arg_model.model_json_schema() +class SerenaMCPFactory: + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + self.context = SerenaAgentContext.load(context) + self.project = project - docstring = docstring_parser.parse(func_doc) + @staticmethod + def make_mcp_tool(tool: ToolInterface) -> MCPTool: + func_name = tool.get_name() + func_doc = tool.get_apply_docstring() or "" + func_arg_metadata = tool.get_apply_fn_metadata() + is_async = False + parameters = func_arg_metadata.arg_model.model_json_schema() - # Mount the tool description as a combination of the docstring description and - # the return value description, if it exists. - if docstring.description: - func_doc = f"{docstring.description.strip().strip('.')}." - else: - func_doc = "" - if docstring.returns and (docstring_returns_descr := docstring.returns.description): - # Only add a space before "Returns" if func_doc is not empty - prefix = " " if func_doc else "" - func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." + docstring = docstring_parser.parse(func_doc) - # Parse the parameter descriptions from the docstring and add pass its description - # to the parameters schema. - docstring_params = {param.arg_name: param for param in docstring.params} - parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] - for parameter, properties in parameters_properties.items(): - if (param_doc := docstring_params.get(parameter)) and param_doc.description: - param_desc = f"{param_doc.description.strip().strip('.') + '.'}" - properties["description"] = param_desc[0].upper() + param_desc[1:] + # Mount the tool description as a combination of the docstring description and + # the return value description, if it exists. + if docstring.description: + func_doc = f"{docstring.description.strip().strip('.')}." + else: + func_doc = "" + if docstring.returns and (docstring_returns_descr := docstring.returns.description): + # Only add a space before "Returns" if func_doc is not empty + prefix = " " if func_doc else "" + func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." - def execute_fn(**kwargs) -> str: # type: ignore - return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) + # Parse the parameter descriptions from the docstring and add pass its description + # to the parameter schema. + docstring_params = {param.arg_name: param for param in docstring.params} + parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] + for parameter, properties in parameters_properties.items(): + if (param_doc := docstring_params.get(parameter)) and param_doc.description: + param_desc = f"{param_doc.description.strip().strip('.') + '.'}" + properties["description"] = param_desc[0].upper() + param_desc[1:] - return MCPTool( - fn=execute_fn, - name=func_name, - description=func_doc, - parameters=parameters, - fn_metadata=func_arg_metadata, - is_async=is_async, - context_kwarg=None, - annotations=None, - ) + def execute_fn(**kwargs) -> str: # type: ignore + return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) + return MCPTool( + fn=execute_fn, + name=func_name, + description=func_doc, + parameters=parameters, + fn_metadata=func_arg_metadata, + is_async=is_async, + context_kwarg=None, + annotations=None, + ) -def create_mcp_server_and_agent( - project: str | None, - host: str = "0.0.0.0", - port: int = 8000, - context: str = DEFAULT_CONTEXT, - modes: Sequence[str] = DEFAULT_MODES, - enable_web_dashboard: bool | None = None, - enable_gui_log_window: bool | None = None, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, - trace_lsp_communication: bool | None = None, - tool_timeout: float | None = None, -) -> tuple[FastMCP, ProcessIsolatedSerenaAgent]: - """ - Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. + @abstractmethod + def _iter_tools(self) -> Iterator[ToolInterface]: + pass - :param project: "Either an absolute path to the project directory or a name of an already registered project. " - "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " - "afterwards. - :param host: The host to bind to - :param port: The port to bind to - :param context: The context name or path to context file - :param modes: List of mode names or paths to mode files - :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. - :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. - If not specified, will take the value from the serena configuration. - :param log_level: Log level. If not specified, will take the value from the serena configuration. - :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. - This is useful for debugging language server issues. - :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. - """ - mcp: FastMCP | None = None - context_instance = SerenaAgentContext.load(context) - modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + # noinspection PyProtectedMember + def _set_mcp_tools(self, mcp: FastMCP) -> None: + """Update the tools in the MCP server""" + if mcp is not None: + mcp._tool_manager._tools = {} + for tool in self._iter_tools(): + mcp_tool = self.make_mcp_tool(tool) + mcp._tool_manager._tools[tool.get_name()] = mcp_tool - is_ide_assistant = context_instance.name == RegisteredContext.IDE_ASSISTANT.value + @abstractmethod + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + pass - project_instance: Project | None = None - if project is not None: - # Fail early if the project cannot be loaded - log.info(f"Will activate project {project} at mcp server startup") + def create_mcp_server( + self, + host: str = "0.0.0.0", + port: int = 8000, + modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ) -> FastMCP: + """ + Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. + + :param host: The host to bind to + :param port: The port to bind to + :param modes: List of mode names or paths to mode files + :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. + :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. + If not specified, will take the value from the serena configuration. + :param log_level: Log level. If not specified, will take the value from the serena configuration. + :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. + This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + """ try: - project_instance = Project.load(project) + serena_config = create_serena_config( + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + self._instantiate_agent(serena_config, modes_instances) + except Exception as e: - log.error(f"Failed to load or generate project config for {project}: {e}") + show_fatal_exception_safe(e) raise - else: - log.warning("No project passed at startup, you will have to activate a project yourself (by asking the agent to do so).") + + # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain + # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; + # retain only FASTMCP_ prefix for already set environment variables. + Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") + + mcp_settings: Settings = Settings(lifespan=self.server_lifespan, host=host, port=port) + mcp = FastMCP(**mcp_settings.model_dump()) + return mcp + + @asynccontextmanager + @abstractmethod + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: + """Manage server startup and shutdown lifecycle.""" + yield None # ensures MyPy understands we yield None + + +class SerenaMCPFactorySingleProcess(SerenaMCPFactory): + """ + MCP server factory where the SerenaAgent and its language server run in the same process as the MCP server + """ + + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + super().__init__(context=context, project=project) + self.agent: SerenaAgent | None = None + + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + self.agent = SerenaAgent(project=self.project, serena_config=serena_config, context=self.context, modes=modes) + + def _iter_tools(self) -> Iterator[ToolInterface]: + assert self.agent is not None + yield from self.agent.get_exposed_tool_instances() + + @asynccontextmanager + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: + self._set_mcp_tools(mcp_server) + yield + + +class SerenaMCPFactoryWithProcessIsolation(SerenaMCPFactory): + """ + MCP server factory with process isolation for the SerenaAgent and its language server; they run in a separate process + from the MCP server. + """ + + def __init__(self, context: str = DEFAULT_CONTEXT, project: str | None = None): + """ + :param context: The context name or path to context file + :param project: Either an absolute path to the project directory or a name of an already registered project. + If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name + afterward. + """ + super().__init__(context=context, project=project) + + self.active_tool_names: set[str] | None = None + self.serena_agent_process: ProcessIsolatedSerenaAgent | None = None + self.serena_dashboard_process: ProcessIsolatedDashboard | None = None + + @staticmethod + def _determine_active_tool_names(context: SerenaAgentContext, project: Project | None) -> set[str]: + """ + Determine the names of tools that should be included in this session based on the context. + """ + tools_excluded_in_this_session = context.get_excluded_tool_classes() - tools_excluded_in_this_session = context_instance.get_excluded_tool_classes() - if is_ide_assistant and project_instance is not None: - tools_excluded_in_this_session.extend(project_instance.project_config.get_excluded_tool_classes()) # if a project has been loaded, it will be activated at startup and in ide-assistant context, # we assume that no other project will be activated in this session. # Therefore, we exclude the activate project tool - tools_excluded_in_this_session.append(ActivateProjectTool) - tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session} + is_ide_assistant = context.name == RegisteredContext.IDE_ASSISTANT.value + if is_ide_assistant and project is not None: + tools_excluded_in_this_session.extend(project.project_config.get_excluded_tool_classes()) + tools_excluded_in_this_session.append(ActivateProjectTool) - all_tool_names = set(ToolRegistry.get_tool_names()) - tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session + tool_names_excluded_in_this_session = {tool.get_name_from_cls() for tool in tools_excluded_in_this_session} - try: - # Start agent and dashboard processes (the latter only if enabled) - serena_dashboard_process = None - serena_config = create_serena_config( - enable_web_dashboard=enable_web_dashboard, - enable_gui_log_window=enable_gui_log_window, - log_level=log_level, - trace_lsp_communication=trace_lsp_communication, - tool_timeout=tool_timeout, - ) - if serena_config.web_dashboard: - serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(tool_names_included_in_this_session)) - serena_agent_process = ProcessIsolatedSerenaAgent( - project=project, serena_config=serena_config, modes=modes_instances, context=context_instance + all_tool_names = set(ToolRegistry.get_tool_names()) + tool_names_included_in_this_session = all_tool_names - tool_names_excluded_in_this_session + return tool_names_included_in_this_session + + @staticmethod + def make_mcp_tool(tool: ToolInterface) -> MCPTool: + func_name = tool.get_name() + func_doc = tool.get_apply_docstring() or "" + func_arg_metadata = tool.get_apply_fn_metadata() + is_async = False + parameters = func_arg_metadata.arg_model.model_json_schema() + + docstring = docstring_parser.parse(func_doc) + + # Mount the tool description as a combination of the docstring description and + # the return value description, if it exists. + if docstring.description: + func_doc = f"{docstring.description.strip().strip('.')}." + else: + func_doc = "" + if docstring.returns and (docstring_returns_descr := docstring.returns.description): + # Only add a space before "Returns" if func_doc is not empty + prefix = " " if func_doc else "" + func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." + + # Parse the parameter descriptions from the docstring and add pass its description + # to the parameter schema. + docstring_params = {param.arg_name: param for param in docstring.params} + parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] + for parameter, properties in parameters_properties.items(): + if (param_doc := docstring_params.get(parameter)) and param_doc.description: + param_desc = f"{param_doc.description.strip().strip('.') + '.'}" + properties["description"] = param_desc[0].upper() + param_desc[1:] + + def execute_fn(**kwargs) -> str: # type: ignore + return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) + + return MCPTool( + fn=execute_fn, + name=func_name, + description=func_doc, + parameters=parameters, + fn_metadata=func_arg_metadata, + is_async=is_async, + context_kwarg=None, + annotations=None, ) - except Exception as e: - show_fatal_exception_safe(e) - raise + def _iter_tools(self) -> Iterator[ToolInterface]: + assert self.active_tool_names is not None + assert self.serena_agent_process is not None + for tool_name in self.active_tool_names: + yield ProcessIsolatedTool(process_agent=self.serena_agent_process, tool_name=tool_name) - def update_tools() -> None: - """Update the tools in the MCP server - adapted for process isolation.""" - nonlocal mcp, serena_agent_process - - # Get tool names from agent - # Tools may change as a result of project or mode activation. - # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, - # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, - # so we need to register all potentially active tools at startup, unfortunately. + # noinspection PyProtectedMember + def _set_mcp_tools(self, mcp: FastMCP) -> None: + """Update the tools in the MCP server""" if mcp is not None: mcp._tool_manager._tools = {} - for tool_name in tool_names_included_in_this_session: - process_isolated_tool = ProcessIsolatedTool(process_agent=serena_agent_process, tool_name=tool_name) - mcp_tool = make_tool(process_isolated_tool) - mcp._tool_manager._tools[tool_name] = mcp_tool + for tool in self._iter_tools(): + mcp_tool = self.make_mcp_tool(tool) + mcp._tool_manager._tools[tool.get_name()] = mcp_tool + + def _instantiate_agent(self, serena_config: SerenaConfig, modes: list[SerenaAgentMode]) -> None: + if self.project is not None: + self.project_instance = serena_config.get_project(self.project) + self.serena_agent_process = ProcessIsolatedSerenaAgent( + project=self.project, serena_config=serena_config, modes=modes, context=self.context + ) + self.active_tool_names = self._determine_active_tool_names(self.context, self.project_instance) + if serena_config.web_dashboard: + assert self.active_tool_names is not None + self.serena_dashboard_process = ProcessIsolatedDashboard(tool_names=sorted(self.active_tool_names)) + + def create_mcp_server( + self, + host: str = "0.0.0.0", + port: int = 8000, + modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ) -> FastMCP: + """ + Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. + + :param host: The host to bind to + :param port: The port to bind to + :param modes: List of mode names or paths to mode files + :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. + :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. + If not specified, will take the value from the serena configuration. + :param log_level: Log level. If not specified, will take the value from the serena configuration. + :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. + This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + """ + try: + serena_config = create_serena_config( + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + self._instantiate_agent(serena_config, modes_instances) + + except Exception as e: + show_fatal_exception_safe(e) + raise + + # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain + # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; + # retain only FASTMCP_ prefix for already set environment variables. + Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") + + mcp_settings = Settings(lifespan=self.server_lifespan, host=host, port=port) + mcp = FastMCP(**mcp_settings.model_dump()) + return mcp @asynccontextmanager - async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: + async def server_lifespan(self, mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - mark_used(mcp_server) def signal_handler(signum: int, frame: Any) -> None: log.info(f"Received signal {signum} in main process") @@ -220,6 +393,8 @@ def create_mcp_server_and_agent( def force_exit() -> None: time.sleep(2.0) # Wait 2 seconds for graceful shutdown log.warning("Forcing exit after timeout") + # noinspection PyProtectedMember + # noinspection PyUnresolvedReferences os._exit(1) threading.Thread(target=force_exit, daemon=True).start() @@ -228,12 +403,15 @@ def create_mcp_server_and_agent( sigint_singal = signal.signal(signal.SIGINT, signal_handler) sigterm_signal = signal.signal(signal.SIGTERM, signal_handler) - if serena_dashboard_process is not None: + if self.serena_dashboard_process is not None: log.info("Starting dashboard process") - serena_dashboard_process.start() + assert self.serena_dashboard_process is not None + self.serena_dashboard_process.start() log.info("Starting serena agent process") - serena_agent_process.start() - update_tools() + assert self.serena_agent_process is not None + self.serena_agent_process.start() + + self._set_mcp_tools(mcp_server) async def monitor_global_shutdown() -> None: """Monitor the global shutdown event and trigger local shutdown.""" @@ -264,23 +442,14 @@ def create_mcp_server_and_agent( with contextlib.suppress(asyncio.CancelledError): await monitor_task - serena_agent_process.stop() - if serena_dashboard_process is not None: - serena_dashboard_process.stop() + self.serena_agent_process.stop() + if self.serena_dashboard_process is not None: + self.serena_dashboard_process.stop() request_global_shutdown() log.info("Shutting down all processes") signal.signal(signal.SIGINT, sigint_singal) signal.signal(signal.SIGTERM, sigterm_signal) - # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain - # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; - # retain only FASTMCP_ prefix for already set environment variables. - Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") - - mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) - mcp = FastMCP(**mcp_settings.model_dump()) - return mcp, serena_agent_process - class ProjectType(click.ParamType): name = "[PROJECT_NAME|PROJECT_PATH]" @@ -417,12 +586,17 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project + mcp_factory: SerenaMCPFactory + if USE_SOLID_LSP: + mcp_factory = SerenaMCPFactorySingleProcess(context=context, project=project_file) + else: + # using multilspy requires process isolation to prevent asyncio contamination + mcp_factory = SerenaMCPFactoryWithProcessIsolation(context=context, project=project_file) + # Use process isolation by default to prevent asyncio event loop contamination - mcp_server, agent = create_mcp_server_and_agent( - project=project_file, + mcp_server = mcp_factory.create_mcp_server( host=host, port=port, - context=context, modes=modes, enable_web_dashboard=enable_web_dashboard, enable_gui_log_window=enable_gui_log_window, @@ -439,6 +613,6 @@ def start_mcp_server( f"Used path: {project_file}" ) - log.info(f"Starting MCP server with config:\n{agent.serena_config}.\n Log level: {agent.serena_config.log_level}") + log.info("Starting MCP server ...") mcp_server.run(transport=transport) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 48d5dc8c..90699b03 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -3,6 +3,7 @@ import contextlib import logging import multiprocessing import os +import threading import traceback import webbrowser from enum import StrEnum @@ -12,7 +13,6 @@ from multiprocessing.sharedctypes import Synchronized from multiprocessing.synchronize import Event as EventClass from typing import Any, Literal, Self -import uvicorn from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry @@ -79,30 +79,21 @@ def _dashboard_worker( port = api._find_first_free_port(0x5EDA) port_value.value = port - # Start server first, then log processing - config = uvicorn.Config( - app=api._app, - host="0.0.0.0", - port=port, - workers=1, - log_config=None, - log_level="critical", - ) - server = uvicorn.Server(config) + # Start Flask server in a thread + def run_flask_server() -> None: + api._app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False, threaded=True) + + server_thread = threading.Thread(target=run_flask_server, daemon=True) + server_thread.start() - # Serve and await shutdown signal - server_task = asyncio.create_task(server.serve()) shutdown_task = asyncio.create_task(_monitor_shutdown()) logging_loop_task = asyncio.create_task(_process_logs(api)) dashboard_ready_event.set() done, pending = await asyncio.wait( - [server_task, shutdown_task, logging_loop_task], + [shutdown_task, logging_loop_task], return_when=asyncio.FIRST_COMPLETED, ) - if shutdown_task in done: - server.should_exit = True - await server_task # Cancel remaining tasks for task in pending: diff --git a/src/serena/util/git.py b/src/serena/util/git.py new file mode 100644 index 00000000..b46d17e2 --- /dev/null +++ b/src/serena/util/git.py @@ -0,0 +1,21 @@ +import logging + +from sensai.util.git import GitStatus + +from .shell import subprocess_check_output + +log = logging.getLogger(__name__) + + +def get_git_status() -> GitStatus | None: + try: + commit_hash = subprocess_check_output(["git", "rev-parse", "HEAD"]) + unstaged = bool(subprocess_check_output(["git", "diff", "--name-only"])) + staged = bool(subprocess_check_output(["git", "diff", "--staged", "--name-only"])) + untracked = bool(subprocess_check_output(["git", "ls-files", "--others", "--exclude-standard"])) + return GitStatus( + commit=commit_hash, has_unstaged_changes=unstaged, has_staged_uncommitted_changes=staged, has_untracked_files=untracked + ) + except Exception as e: + log.error("Error determining Git status", exc_info=e) + return None diff --git a/src/serena/util/shell.py b/src/serena/util/shell.py index d940384d..c899b212 100644 --- a/src/serena/util/shell.py +++ b/src/serena/util/shell.py @@ -24,11 +24,14 @@ def execute_shell_command(command: str, cwd: str | None = None, capture_stderr: if cwd is None: cwd = os.getcwd() + is_windows = platform.system() == "Windows" process = subprocess.Popen( command, - shell=platform.system() != "Windows", + shell=not is_windows, + stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE if capture_stderr else None, + creationflags=subprocess.CREATE_NO_WINDOW if is_windows else 0, # type: ignore text=True, encoding="utf-8", errors="replace", @@ -37,3 +40,18 @@ def execute_shell_command(command: str, cwd: str | None = None, capture_stderr: stdout, stderr = process.communicate() return ShellCommandResult(stdout=stdout, stderr=stderr, return_code=process.returncode, cwd=cwd) + + +def subprocess_check_output(args: list[str], encoding: str = "utf-8", strip: bool = True, timeout: float | None = None) -> str: + kwargs = { + "stdin": subprocess.DEVNULL, + "stderr": subprocess.PIPE, + "timeout": timeout, + "env": os.environ.copy(), + } + if platform.system() == "Windows": + kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW # type: ignore + output = subprocess.check_output(args, **kwargs).decode(encoding) # type: ignore + if strip: + output = output.strip() + return output diff --git a/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py new file mode 100644 index 00000000..0ee5f471 --- /dev/null +++ b/src/solidlsp/language_servers/clangd_language_server/clangd_language_server.py @@ -0,0 +1,188 @@ +""" +Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. +""" + +import json +import logging +import os +import pathlib +import stat +import threading + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer + + +class ClangdLanguageServer(SolidLanguageServer): + """ + Provides C/C++ specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C/C++. + As the project gets bigger in size, building index will take time. Try running clangd multiple times to ensure index is built properly. + Also make sure compile_commands.json is created at root of the source directory. Check clangd test case for example. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a ClangdLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + clangd_executable_path = self.setup_runtime_dependencies(logger, config) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=clangd_executable_path, cwd=repository_root_path), + "cpp", + ) + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for ClangdLanguageServer. + """ + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + d = json.load(f) + del d["_description"] + + assert platform_id.value in [ + "linux-x64", + "win-x64", + "osx-arm64", + ], "Unsupported platform: " + platform_id.value + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + assert len(runtime_dependencies) == 1 + # Select dependency matching the current platform + dependency = next((dep for dep in runtime_dependencies if dep["platformId"] == platform_id.value), None) + if dependency is None: + raise RuntimeError(f"No runtime dependency found for platform {platform_id.value}") + + clangd_ls_dir = os.path.join(os.path.dirname(__file__), "static", "clangd") + clangd_executable_path = os.path.join(clangd_ls_dir, "clangd_19.1.2", "bin", dependency["binaryName"]) + if not os.path.exists(clangd_executable_path): + clangd_url = dependency["url"] + logger.log(f"Clangd executable not found at {clangd_executable_path}. Downloading from {clangd_url}", logging.INFO) + os.makedirs(clangd_ls_dir, exist_ok=True) + if dependency["archiveType"] == "zip": + FileUtils.download_and_extract_archive( + logger, clangd_url, clangd_ls_dir, dependency["archiveType"] + ) + else: + raise RuntimeError(f"Unsupported archive type: {dependency['archiveType']}") + if not os.path.exists(clangd_executable_path): + raise FileNotFoundError( + f"Clangd executable not found at {clangd_executable_path}.\n" + "Make sure you have installed clangd. See https://clangd.llvm.org/installation" + ) + os.chmod(clangd_executable_path, stat.S_IEXEC) + + return clangd_executable_path + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the clangd Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """ + Starts the Clangd Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + """ + def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + self.resolve_main_method_available.set() + return + + def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting Clangd server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": ['.', '<', '>', ':', '"', '/', '*'], + "resolveProvider": False, + } + + self.server.notify.initialized({}) + + self.completions_available.set() + # set ready flag + self.server_ready.set() + self.server_ready.wait() + diff --git a/src/solidlsp/language_servers/clangd_language_server/initialize_params.json b/src/solidlsp/language_servers/clangd_language_server/initialize_params.json new file mode 100644 index 00000000..4330560e --- /dev/null +++ b/src/solidlsp/language_servers/clangd_language_server/initialize_params.json @@ -0,0 +1,36 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "textDocument": { + "synchronization": { + "didSave": true, + "dynamicRegistration": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "definition": { + "dynamicRegistration": true + } + }, + "workspace": { + "workspaceFolders": true, + "didChangeConfiguration": { + "dynamicRegistration": true + } + } + }, + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json new file mode 100644 index 00000000..53f5b7f3 --- /dev/null +++ b/src/solidlsp/language_servers/clangd_language_server/runtime_dependencies.json @@ -0,0 +1,29 @@ +{ + "_description": "Used to download the runtime dependencies for running Clangd.", + "runtimeDependencies": [ + { + "id": "Clangd", + "description": "Clangd for Linux (x64)", + "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-linux-19.1.2.zip", + "platformId": "linux-x64", + "archiveType": "zip", + "binaryName": "clangd" + }, + { + "id": "Clangd", + "description": "Clangd for Windows (x64)", + "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-windows-19.1.2.zip", + "platformId": "windows-x64", + "archiveType": "zip", + "binaryName": "clangd.exe" + }, + { + "id": "Clangd", + "description": "Clangd for macOS (Arm64)", + "url": "https://github.com/clangd/clangd/releases/download/19.1.2/clangd-mac-19.1.2.zip", + "platformId": "osx-arm64", + "archiveType": "zip", + "binaryName": "clangd" + } + ] +} diff --git a/src/solidlsp/language_servers/dart_language_server/dart_language_server.py b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py new file mode 100644 index 00000000..2a0c4e66 --- /dev/null +++ b/src/solidlsp/language_servers/dart_language_server/dart_language_server.py @@ -0,0 +1,138 @@ +import json +import logging +import os +import pathlib +import stat + +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import FileUtils, PlatformUtils +from solidlsp.ls import SolidLanguageServer + + +class DartLanguageServer(SolidLanguageServer): + """ + Provides Dart specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Dart. + """ + + def __init__(self, config, logger, repository_root_path): + """ + Creates a DartServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + + executable_path = self.setup_runtime_dependencies(logger) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=executable_path, cwd=repository_root_path), + "dart", + ) + + def setup_runtime_dependencies(self, logger: "MultilspyLogger") -> str: + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r") as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + + assert len(runtime_dependencies) == 1 + dependency = runtime_dependencies[0] + + dart_ls_dir = os.path.join(os.path.dirname(__file__), "static", "dart-language-server") + dart_executable_path = os.path.join(dart_ls_dir, dependency["binaryName"]) + + if not os.path.exists(dart_ls_dir): + os.makedirs(dart_ls_dir) + FileUtils.download_and_extract_archive( + logger, dependency["url"], dart_ls_dir, dependency["archiveType"] + ) + + + assert os.path.exists(dart_executable_path) + os.chmod(dart_executable_path, stat.S_IEXEC) + + return f"{dart_executable_path} language-server --client-id multilspy.dart --client-version 1.2" + + + def _get_initialize_params(self, repository_absolute_path: str): + """ + Returns the initialize params for the Dart Language Server. + """ + with open( + os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r" + ) as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path( + repository_absolute_path + ).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """ + Start the language server and yield when the server is ready. + """ + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def check_experimental_status(params): + pass + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request( + "workspace/executeClientCommand", execute_client_command_handler + ) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification( + "experimental/serverStatus", check_experimental_status + ) + + self.logger.log( + "Starting dart-language-server server process", logging.INFO + ) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + self.logger.log( + "Sending initialize request to dart-language-server", + logging.DEBUG, + ) + init_response = self.server.send_request( + "initialize", initialize_params + ) + self.logger.log( + f"Received initialize response from dart-language-server: {init_response}", + logging.INFO, + ) + + self.server.notify.initialized({}) diff --git a/src/solidlsp/language_servers/dart_language_server/initialize_params.json b/src/solidlsp/language_servers/dart_language_server/initialize_params.json new file mode 100644 index 00000000..e90e408a --- /dev/null +++ b/src/solidlsp/language_servers/dart_language_server/initialize_params.json @@ -0,0 +1,23 @@ +{ + "_description": "This file contains the initialization parameters for the Dart Language Server.", + "processId": "$processId", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": {}, + "initializationOptions": { + "onlyAnalyzeProjectsWithOpenFiles": false, + "suggestFromUnimportedLibraries": true, + "closingLabels": false, + "outline": false, + "flutterOutline": false, + "allowOpenUri": false + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] + +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json new file mode 100644 index 00000000..32e49299 --- /dev/null +++ b/src/solidlsp/language_servers/dart_language_server/runtime_dependencies.json @@ -0,0 +1,45 @@ +{ + "_description": "Used to download the runtime dependencies for running Dart Language Server, downloaded from https://dart.dev/get-dart/archive", + "runtimeDependencies": [ + { + "id": "DartLanguageServer", + "description": "Dart Language Server for Linux (x64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-linux-x64-release.zip", + "platformId": "linux-x64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart" + }, + { + "id": "DartLanguageServer", + "description": "Dart Language Server for Windows (x64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-x64-release.zip", + "platformId": "win-x64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart.exe" + }, + { + "id": "DartLanguageServer", + "description": "Dart Language Server for Windows (arm64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-windows-arm64-release.zip", + "platformId": "win-arm64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart.exe" + }, + { + "id": "DartLanguageServer", + "description": "Dart Language Server for macOS (x64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-x64-release.zip", + "platformId": "osx-x64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart" + }, + { + "id": "DartLanguageServer", + "description": "Dart Language Server for macOS (arm64)", + "url": "https://storage.googleapis.com/dart-archive/channels/stable/release/3.7.1/sdk/dartsdk-macos-arm64-release.zip", + "platformId": "osx-arm64", + "archiveType": "zip", + "binaryName": "dart-sdk/bin/dart" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py new file mode 100644 index 00000000..1b92fc4d --- /dev/null +++ b/src/solidlsp/language_servers/eclipse_jdtls/eclipse_jdtls.py @@ -0,0 +1,406 @@ +""" +Provides Java specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Java. +""" + +import dataclasses +import json +import logging +import os +import pathlib +import shutil +import stat +import threading +import uuid +from pathlib import PurePath + +from overrides import override + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_settings import MultilspySettings +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer + + +@dataclasses.dataclass +class RuntimeDependencyPaths: + """ + Stores the paths to the runtime dependencies of EclipseJDTLS + """ + + gradle_path: str + lombok_jar_path: str + jre_path: str + jre_home_path: str + jdtls_launcher_jar_path: str + jdtls_readonly_config_path: str + intellicode_jar_path: str + intellisense_members_path: str + + +class EclipseJDTLS(SolidLanguageServer): + """ + The EclipseJDTLS class provides a Java specific implementation of the LanguageServer class + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a new EclipseJDTLS instance initializing the language server settings appropriately. + This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + + runtime_dependency_paths = self.setupRuntimeDependencies(logger, config) + self.runtime_dependency_paths = runtime_dependency_paths + + # ws_dir is the workspace directory for the EclipseJDTLS server + ws_dir = str( + PurePath( + MultilspySettings.get_language_server_directory(), + "EclipseJDTLS", + "workspaces", + uuid.uuid4().hex, + ) + ) + + # shared_cache_location is the global cache used by Eclipse JDTLS across all workspaces + shared_cache_location = str( + PurePath(MultilspySettings.get_global_cache_directory(), "lsp", "EclipseJDTLS", "sharedIndex") + ) + + jre_path = self.runtime_dependency_paths.jre_path + lombok_jar_path = self.runtime_dependency_paths.lombok_jar_path + + jdtls_launcher_jar = self.runtime_dependency_paths.jdtls_launcher_jar_path + + os.makedirs(ws_dir, exist_ok=True) + + data_dir = str(PurePath(ws_dir, "data_dir")) + jdtls_config_path = str(PurePath(ws_dir, "config_path")) + + jdtls_readonly_config_path = self.runtime_dependency_paths.jdtls_readonly_config_path + + if not os.path.exists(jdtls_config_path): + shutil.copytree(jdtls_readonly_config_path, jdtls_config_path) + + for static_path in [ + jre_path, + lombok_jar_path, + jdtls_launcher_jar, + jdtls_config_path, + jdtls_readonly_config_path, + ]: + assert os.path.exists(static_path), static_path + + # TODO: Add "self.runtime_dependency_paths.jre_home_path"/bin to $PATH as well + proc_env = {"syntaxserver": "false", "JAVA_HOME": self.runtime_dependency_paths.jre_home_path} + proc_cwd = repository_root_path + cmd = " ".join( + [ + jre_path, + "--add-modules=ALL-SYSTEM", + "--add-opens", + "java.base/java.util=ALL-UNNAMED", + "--add-opens", + "java.base/java.lang=ALL-UNNAMED", + "--add-opens", + "java.base/sun.nio.fs=ALL-UNNAMED", + "-Declipse.application=org.eclipse.jdt.ls.core.id1", + "-Dosgi.bundles.defaultStartLevel=4", + "-Declipse.product=org.eclipse.jdt.ls.core.product", + "-Djava.import.generatesMetadataFilesAtProjectRoot=false", + "-Dfile.encoding=utf8", + "-noverify", + "-XX:+UseParallelGC", + "-XX:GCTimeRatio=4", + "-XX:AdaptiveSizePolicyWeight=90", + "-Dsun.zip.disableMemoryMapping=true", + "-Djava.lsp.joinOnCompletion=true", + "-Xmx3G", + "-Xms100m", + "-Xlog:disable", + "-Dlog.level=ALL", + f'"-javaagent:{lombok_jar_path}"', + f'"-Djdt.core.sharedIndexLocation={shared_cache_location}"', + "-jar", + f'"{jdtls_launcher_jar}"', + "-configuration", + f'"{jdtls_config_path}"', + "-data", + f'"{data_dir}"', + ] + ) + + self.service_ready_event = threading.Event() + self.intellicode_enable_command_available = threading.Event() + self.initialize_searcher_command_available = threading.Event() + + super().__init__(config, logger, repository_root_path, ProcessLaunchInfo(cmd, proc_env, proc_cwd), "java") + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # Ignore common Java build directories from different build tools: + # - Maven: target + # - Gradle: build, .gradle + # - Eclipse: bin, .settings + # - IntelliJ IDEA: out, .idea + # - General: classes, dist, lib + return super().is_ignored_dirname(dirname) or dirname in [ + "target", # Maven + "build", # Gradle + "bin", # Eclipse + "out", # IntelliJ IDEA + "classes", # General + "dist", # General + "lib" # General + ] + + def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> RuntimeDependencyPaths: + """ + Setup runtime dependencies for EclipseJDTLS. + """ + platformId = PlatformUtils.get_platform_id() + + with open(str(PurePath(os.path.dirname(__file__), "runtime_dependencies.json")), "r", encoding="utf-8") as f: + runtimeDependencies = json.load(f) + del runtimeDependencies["_description"] + + os.makedirs(str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static")), exist_ok=True) + + # assert platformId.value in [ + # "linux-x64", + # "win-x64", + # ], "Only linux-x64 platform is supported for in multilspy at the moment" + + gradle_path = str( + PurePath( + os.path.abspath(os.path.dirname(__file__)), + "static/gradle-7.3.3", + ) + ) + + if not os.path.exists(gradle_path): + FileUtils.download_and_extract_archive( + logger, + runtimeDependencies["gradle"]["platform-agnostic"]["url"], + str(PurePath(gradle_path).parent), + runtimeDependencies["gradle"]["platform-agnostic"]["archiveType"], + ) + + assert os.path.exists(gradle_path) + + dependency = runtimeDependencies["vscode-java"][platformId.value] + vscode_java_path = str( + PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]) + ) + os.makedirs(vscode_java_path, exist_ok=True) + jre_home_path = str(PurePath(vscode_java_path, dependency["jre_home_path"])) + jre_path = str(PurePath(vscode_java_path, dependency["jre_path"])) + lombok_jar_path = str(PurePath(vscode_java_path, dependency["lombok_jar_path"])) + jdtls_launcher_jar_path = str(PurePath(vscode_java_path, dependency["jdtls_launcher_jar_path"])) + jdtls_readonly_config_path = str(PurePath(vscode_java_path, dependency["jdtls_readonly_config_path"])) + if not all( + [ + os.path.exists(vscode_java_path), + os.path.exists(jre_home_path), + os.path.exists(jre_path), + os.path.exists(lombok_jar_path), + os.path.exists(jdtls_launcher_jar_path), + os.path.exists(jdtls_readonly_config_path), + ] + ): + FileUtils.download_and_extract_archive( + logger, dependency["url"], vscode_java_path, dependency["archiveType"] + ) + + os.chmod(jre_path, stat.S_IEXEC) + + assert os.path.exists(vscode_java_path) + assert os.path.exists(jre_home_path) + assert os.path.exists(jre_path) + assert os.path.exists(lombok_jar_path) + assert os.path.exists(jdtls_launcher_jar_path) + assert os.path.exists(jdtls_readonly_config_path) + + dependency = runtimeDependencies["intellicode"]["platform-agnostic"] + intellicode_directory_path = str( + PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]) + ) + os.makedirs(intellicode_directory_path, exist_ok=True) + intellicode_jar_path = str(PurePath(intellicode_directory_path, dependency["intellicode_jar_path"])) + intellisense_members_path = str(PurePath(intellicode_directory_path, dependency["intellisense_members_path"])) + if not all( + [ + os.path.exists(intellicode_directory_path), + os.path.exists(intellicode_jar_path), + os.path.exists(intellisense_members_path), + ] + ): + FileUtils.download_and_extract_archive( + logger, dependency["url"], intellicode_directory_path, dependency["archiveType"] + ) + + assert os.path.exists(intellicode_directory_path) + assert os.path.exists(intellicode_jar_path) + assert os.path.exists(intellisense_members_path) + + return RuntimeDependencyPaths( + gradle_path=gradle_path, + lombok_jar_path=lombok_jar_path, + jre_path=jre_path, + jre_home_path=jre_home_path, + jdtls_launcher_jar_path=jdtls_launcher_jar_path, + jdtls_readonly_config_path=jdtls_readonly_config_path, + intellicode_jar_path=intellicode_jar_path, + intellisense_members_path=intellisense_members_path, + ) + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize parameters for the EclipseJDTLS server. + """ + # Look into https://github.com/eclipse/eclipse.jdt.ls/blob/master/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/preferences/Preferences.java to understand all the options available + with open(str(PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: + d: InitializeParams = json.load(f) + + del d["_description"] + + if not os.path.isabs(repository_absolute_path): + repository_absolute_path = os.path.abspath(repository_absolute_path) + + assert d["processId"] == "os.getpid()" + d["processId"] = os.getpid() + + assert d["rootPath"] == "repository_absolute_path" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]" + d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] + + assert ( + d["workspaceFolders"] + == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' + ) + d["workspaceFolders"] = [ + { + "uri": pathlib.Path(repository_absolute_path).as_uri(), + "name": os.path.basename(repository_absolute_path), + } + ] + + assert d["initializationOptions"]["bundles"] == ["intellicode-core.jar"] + bundles = [self.runtime_dependency_paths.intellicode_jar_path] + d["initializationOptions"]["bundles"] = bundles + + assert d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] == [ + {"name": "JavaSE-21", "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", "default": True} + ] + d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [ + {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True} + ] + + for runtime in d["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]: + assert "name" in runtime + assert "path" in runtime + assert os.path.exists( + runtime["path"] + ), f"Runtime required for eclipse_jdtls at path {runtime['path']} does not exist" + + assert d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["home"] == "abs(static/gradle-7.3.3)" + d["initializationOptions"]["settings"]["java"]["import"]["gradle"][ + "home" + ] = self.runtime_dependency_paths.gradle_path + + d["initializationOptions"]["settings"]["java"]["import"]["gradle"]["java"][ + "home" + ] = self.runtime_dependency_paths.jre_path + + return d + + def _start_server(self): + """ + Starts the Eclipse JDTLS Language Server + """ + + def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "textDocument/completion": + assert registration["registerOptions"]["resolveProvider"] == True + assert registration["registerOptions"]["triggerCharacters"] == [ + ".", + "@", + "#", + "*", + " ", + ] + self.completions_available.set() + if registration["method"] == "workspace/executeCommand": + if "java.intellicode.enable" in registration["registerOptions"]["commands"]: + self.intellicode_enable_command_available.set() + return + + def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + def execute_client_command_handler(params): + assert params["command"] == "_java.reloadBundles.command" + assert params["arguments"] == [] + return [] + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + + self.logger.log("Starting EclipseJDTLS server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" not in init_response["capabilities"] + assert "executeCommandProvider" not in init_response["capabilities"] + + self.server.notify.initialized({}) + + self.server.notify.workspace_did_change_configuration( + {"settings": initialize_params["initializationOptions"]["settings"]} + ) + + self.intellicode_enable_command_available.wait() + + java_intellisense_members_path = self.runtime_dependency_paths.intellisense_members_path + assert os.path.exists(java_intellisense_members_path) + intellicode_enable_result = self.server.send.execute_command( + { + "command": "java.intellicode.enable", + "arguments": [True, java_intellisense_members_path], + } + ) + assert intellicode_enable_result + + # TODO: Add comments about why we wait here, and how this can be optimized + self.service_ready_event.wait() diff --git a/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json b/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json new file mode 100644 index 00000000..b65b7b62 --- /dev/null +++ b/src/solidlsp/language_servers/eclipse_jdtls/initialize_params.json @@ -0,0 +1,849 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.77.0-insider" + }, + "locale": "en", + "rootPath": "repository_absolute_path", + "rootUri": "pathlib.Path(repository_absolute_path).as_uri()", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "configuration": true, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": false, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": false, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": true + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0" + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + } + }, + "initializationOptions": { + "bundles": [ + "intellicode-core.jar" + ], + "workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]", + "settings": { + "java": { + "home": null, + "jdt": { + "ls": { + "java": { + "home": null + }, + "vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx1G -Xms100m -Xlog:disable", + "lombokSupport": { + "enabled": true + }, + "protobufSupport": { + "enabled": true + }, + "androidSupport": { + "enabled": true + } + } + }, + "errors": { + "incompleteClasspath": { + "severity": "error" + } + }, + "configuration": { + "checkProjectSettingsExclusions": false, + "updateBuildConfiguration": "interactive", + "maven": { + "userSettings": null, + "globalSettings": null, + "notCoveredPluginExecutionSeverity": "warning", + "defaultMojoExecutionAction": "ignore" + }, + "workspaceCacheLimit": 90, + "runtimes": [ + { + "name": "JavaSE-21", + "path": "static/vscode-java/extension/jre/21.0.7-linux-x86_64", + "default": true + } + ] + }, + "trace": { + "server": "verbose" + }, + "import": { + "maven": { + "enabled": true, + "offline": { + "enabled": false + }, + "disableTestClasspathFlag": false + }, + "gradle": { + "enabled": true, + "wrapper": { + "enabled": true + }, + "version": null, + "home": "abs(static/gradle-7.3.3)", + "java": { + "home": "abs(static/launch_jres/21.0.7-linux-x86_64)" + }, + "offline": { + "enabled": false + }, + "arguments": null, + "jvmArguments": null, + "user": { + "home": null + }, + "annotationProcessing": { + "enabled": true + } + }, + "exclusions": [ + "**/node_modules/**", + "**/.metadata/**", + "**/archetype-resources/**", + "**/META-INF/maven/**" + ], + "generatesMetadataFilesAtProjectRoot": false + }, + "maven": { + "downloadSources": true, + "updateSnapshots": true + }, + "eclipse": { + "downloadSources": true + }, + "referencesCodeLens": { + "enabled": true + }, + "signatureHelp": { + "enabled": true, + "description": { + "enabled": true + } + }, + "implementationsCodeLens": { + "enabled": true + }, + "format": { + "enabled": true, + "settings": { + "url": null, + "profile": null + }, + "comments": { + "enabled": true + }, + "onType": { + "enabled": true + }, + "insertSpaces": true, + "tabSize": 4 + }, + "saveActions": { + "organizeImports": false + }, + "project": { + "referencedLibraries": [ + "lib/**/*.jar" + ], + "importOnFirstTimeStartup": "automatic", + "importHint": true, + "resourceFilters": [ + "node_modules", + "\\.git" + ], + "encoding": "ignore", + "exportJar": { + "targetPath": "${workspaceFolder}/${workspaceFolderBasename}.jar" + } + }, + "contentProvider": { + "preferred": null + }, + "autobuild": { + "enabled": true + }, + "maxConcurrentBuilds": 1, + "recommendations": { + "dependency": { + "analytics": { + "show": true + } + } + }, + "completion": { + "maxResults": 0, + "enabled": true, + "guessMethodArguments": true, + "favoriteStaticMembers": [ + "org.junit.Assert.*", + "org.junit.Assume.*", + "org.junit.jupiter.api.Assertions.*", + "org.junit.jupiter.api.Assumptions.*", + "org.junit.jupiter.api.DynamicContainer.*", + "org.junit.jupiter.api.DynamicTest.*", + "org.mockito.Mockito.*", + "org.mockito.ArgumentMatchers.*", + "org.mockito.Answers.*" + ], + "filteredTypes": [ + "java.awt.*", + "com.sun.*", + "sun.*", + "jdk.*", + "org.graalvm.*", + "io.micrometer.shaded.*" + ], + "importOrder": [ + "#", + "java", + "javax", + "org", + "com", + "" + ], + "postfix": { + "enabled": false + }, + "matchCase": "off" + }, + "foldingRange": { + "enabled": true + }, + "progressReports": { + "enabled": false + }, + "codeGeneration": { + "hashCodeEquals": { + "useJava7Objects": false, + "useInstanceof": false + }, + "useBlocks": false, + "generateComments": false, + "toString": { + "template": "${object.className} [${member.name()}=${member.value}, ${otherMembers}]", + "codeStyle": "STRING_CONCATENATION", + "skipNullValues": false, + "listArrayContents": true, + "limitElements": 0 + }, + "insertionLocation": "afterCursor" + }, + "selectionRange": { + "enabled": true + }, + "showBuildStatusOnStart": { + "enabled": "notification" + }, + "server": { + "launchMode": "Standard" + }, + "sources": { + "organizeImports": { + "starThreshold": 99, + "staticStarThreshold": 99 + } + }, + "imports": { + "gradle": { + "wrapper": { + "checksums": [] + } + } + }, + "templates": { + "fileHeader": [], + "typeComment": [] + }, + "references": { + "includeAccessors": true, + "includeDecompiledSources": true + }, + "typeHierarchy": { + "lazyLoad": false + }, + "settings": { + "url": null + }, + "symbols": { + "includeSourceMethodDeclarations": false + }, + "quickfix": { + "showAt": "line" + }, + "inlayHints": { + "parameterNames": { + "enabled": "literals", + "exclusions": [] + } + }, + "codeAction": { + "sortMembers": { + "avoidVolatileChanges": true + } + }, + "compile": { + "nullAnalysis": { + "nonnull": [ + "javax.annotation.Nonnull", + "org.eclipse.jdt.annotation.NonNull", + "org.springframework.lang.NonNull" + ], + "nullable": [ + "javax.annotation.Nullable", + "org.eclipse.jdt.annotation.Nullable", + "org.springframework.lang.Nullable" + ], + "mode": "automatic" + } + }, + "cleanup": { + "actionsOnSave": [] + }, + "sharedIndexes": { + "enabled": "auto", + "location": "" + }, + "refactoring": { + "extract": { + "interface": { + "replace": true + } + } + }, + "debug": { + "logLevel": "verbose", + "settings": { + "showHex": false, + "showStaticVariables": false, + "showQualifiedNames": false, + "showLogicalStructure": true, + "showToString": true, + "maxStringLength": 0, + "numericPrecision": 0, + "hotCodeReplace": "manual", + "enableRunDebugCodeLens": true, + "forceBuildBeforeLaunch": true, + "onBuildFailureProceed": false, + "console": "integratedTerminal", + "exceptionBreakpoint": { + "skipClasses": [] + }, + "stepping": { + "skipClasses": [], + "skipSynthetics": false, + "skipStaticInitializers": false, + "skipConstructors": false + }, + "jdwp": { + "limitOfVariablesPerJdwpRequest": 100, + "requestTimeout": 3000, + "async": "auto" + }, + "vmArgs": "" + } + }, + "silentNotification": false, + "dependency": { + "showMembers": false, + "syncWithFolderExplorer": true, + "autoRefresh": true, + "refreshDelay": 2000, + "packagePresentation": "flat" + }, + "help": { + "firstView": "auto", + "showReleaseNotes": true, + "collectErrorLog": false + }, + "test": { + "defaultConfig": "", + "config": {} + } + } + }, + "extendedClientCapabilities": { + "progressReportProvider": false, + "classFileContentsSupport": true, + "overrideMethodsPromptSupport": true, + "hashCodeEqualsPromptSupport": true, + "advancedOrganizeImportsSupport": true, + "generateToStringPromptSupport": true, + "advancedGenerateAccessorsSupport": true, + "generateConstructorsPromptSupport": true, + "generateDelegateMethodsPromptSupport": true, + "advancedExtractRefactoringSupport": true, + "inferSelectionSupport": [ + "extractMethod", + "extractVariable", + "extractField" + ], + "moveRefactoringSupport": true, + "clientHoverProvider": true, + "clientDocumentSymbolProvider": true, + "gradleChecksumWrapperPromptSupport": true, + "resolveAdditionalTextEditsSupport": true, + "advancedIntroduceParameterRefactoringSupport": true, + "actionableRuntimeNotificationSupport": true, + "shouldLanguageServerExitOnShutdown": true, + "onCompletionItemSelectedCommand": "editor.action.triggerParameterHints", + "extractInterfaceSupport": true, + "advancedUpgradeGradleSupport": true + }, + "triggerFiles": [] + }, + "trace": "verbose", + "workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]" +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json b/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json new file mode 100644 index 00000000..1b000405 --- /dev/null +++ b/src/solidlsp/language_servers/eclipse_jdtls/runtime_dependencies.json @@ -0,0 +1,72 @@ +{ + "_description": "This file lists the runtime dependencies for the Java Language Server", + "gradle": { + "platform-agnostic": { + "url": "https://services.gradle.org/distributions/gradle-7.3.3-bin.zip", + "archiveType": "zip", + "relative_extraction_path": "." + } + }, + "vscode-java": { + "darwin-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java" + }, + "osx-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-macosx-aarch64", + "jre_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_mac_arm" + }, + "osx-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-macosx-x86_64", + "jre_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_mac" + }, + "linux-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java" + }, + "linux-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-linux-x86_64", + "jre_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_linux" + }, + "win-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "archiveType": "zip", + "relative_extraction_path": "vscode-java", + "jre_home_path": "extension/jre/21.0.7-win32-x86_64", + "jre_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + "lombok_jar_path": "extension/lombok/lombok-1.18.36.jar", + "jdtls_launcher_jar_path": "extension/server/plugins/org.eclipse.equinox.launcher_1.7.0.v20250424-1814.jar", + "jdtls_readonly_config_path": "extension/server/config_win" + } + }, + "intellicode": { + "platform-agnostic": { + "url": "https://VisualStudioExptTeam.gallery.vsassets.io/_apis/public/gallery/publisher/VisualStudioExptTeam/extension/vscodeintellicode/1.2.30/assetbyname/Microsoft.VisualStudio.Services.VSIXPackage", + "alternate_url": "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/VisualStudioExptTeam/vsextensions/vscodeintellicode/1.2.30/vspackage", + "archiveType": "zip", + "relative_extraction_path": "intellicode", + "intellicode_jar_path": "extension/dist/com.microsoft.jdtls.intellicode.core-0.7.0.jar", + "intellisense_members_path": "extension/dist/bundledModels/java_intellisense-members" + } + } +} diff --git a/src/solidlsp/language_servers/gopls/gopls.py b/src/solidlsp/language_servers/gopls/gopls.py new file mode 100644 index 00000000..a2ec2722 --- /dev/null +++ b/src/solidlsp/language_servers/gopls/gopls.py @@ -0,0 +1,145 @@ +import json +import logging +import os +import pathlib +import subprocess +import threading + +from overrides import override + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.ls import SolidLanguageServer + + +class Gopls(SolidLanguageServer): + """ + Provides Go specific instantiation of the LanguageServer class using gopls. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # For Go projects, we should ignore: + # - vendor: third-party dependencies vendored into the project + # - node_modules: if the project has JavaScript components + # - dist/build: common output directories + return super().is_ignored_dirname(dirname) or dirname in ["vendor", "node_modules", "dist", "build"] + + @staticmethod + def _get_go_version(): + """Get the installed Go version or None if not found.""" + try: + result = subprocess.run(['go', 'version'], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + @staticmethod + def _get_gopls_version(): + """Get the installed gopls version or None if not found.""" + try: + result = subprocess.run(['gopls', 'version'], capture_output=True, text=True) + if result.returncode == 0: + return result.stdout.strip() + except FileNotFoundError: + return None + return None + + @classmethod + def setup_runtime_dependency(cls): + """ + Check if required Go runtime dependencies are available. + Raises RuntimeError with helpful message if dependencies are missing. + """ + go_version = cls._get_go_version() + if not go_version: + raise RuntimeError("Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH.") + + gopls_version = cls._get_gopls_version() + if not gopls_version: + raise RuntimeError( + "Found a Go version but gopls is not installed.\n" + "Please install gopls as described in https://pkg.go.dev/golang.org/x/tools/gopls#section-readme\n\n" + "After installation, make sure it is added to your PATH (it might be installed in a different location than Go)." + ) + + return True + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + self.setup_runtime_dependency() + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd="gopls", cwd=repository_root_path), + "go", + ) + self.server_ready = threading.Event() + self.request_id = 0 + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the TypeScript Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """Start gopls server process""" + def register_capability_handler(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + self.logger.log("Starting gopls server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + self.server.notify.initialized({}) + self.completions_available.set() + + # gopls server is typically ready immediately after initialization + self.server_ready.set() + self.server_ready.wait() diff --git a/src/solidlsp/language_servers/gopls/initialize_params.json b/src/solidlsp/language_servers/gopls/initialize_params.json new file mode 100644 index 00000000..40592e1a --- /dev/null +++ b/src/solidlsp/language_servers/gopls/initialize_params.json @@ -0,0 +1,46 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "textDocument": { + "synchronization": { + "didSave": true, + "dynamicRegistration": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "definition": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "hierarchicalDocumentSymbolSupport": true, + "symbolKind": { + "valueSet": [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26 + ] + } + } + }, + "workspace": { + "workspaceFolders": true, + "didChangeConfiguration": { + "dynamicRegistration": true + } + } + }, + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/intelephense/initialize_params.json b/src/solidlsp/language_servers/intelephense/initialize_params.json new file mode 100644 index 00000000..4330560e --- /dev/null +++ b/src/solidlsp/language_servers/intelephense/initialize_params.json @@ -0,0 +1,36 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "textDocument": { + "synchronization": { + "didSave": true, + "dynamicRegistration": true + }, + "completion": { + "dynamicRegistration": true, + "completionItem": { + "snippetSupport": true + } + }, + "definition": { + "dynamicRegistration": true + } + }, + "workspace": { + "workspaceFolders": true, + "didChangeConfiguration": { + "dynamicRegistration": true + } + } + }, + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py new file mode 100644 index 00000000..9b6eb955 --- /dev/null +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -0,0 +1,192 @@ +""" +Provides PHP specific instantiation of the LanguageServer class using Intelephense. +""" + +import json +import logging +import os +import pathlib +import shutil +import subprocess +from time import sleep + +from overrides import override + +from multilspy.lsp_protocol_handler.lsp_types import DefinitionParams, InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import PlatformUtils, PlatformId +from solidlsp.ls import SolidLanguageServer + + +class Intelephense(SolidLanguageServer): + """ + Provides PHP specific instantiation of the LanguageServer class using Intelephense. + """ + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + # For PHP projects, we should ignore: + # - vendor: third-party dependencies managed by Composer + # - node_modules: if the project has JavaScript components + # - cache: commonly used for caching + return super().is_ignored_dirname(dirname) or dirname in ["node_modules", "vendor", "cache"] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for Intelephense. + """ + platform_id = PlatformUtils.get_platform_id() + + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.LINUX_arm64, + PlatformId.OSX, + PlatformId.OSX_x64, + PlatformId.OSX_arm64, + PlatformId.WIN_x64, + PlatformId.WIN_arm64, + ] + assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy PHP at the moment" + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d.get("runtimeDependencies", []) + intelephense_ls_dir = os.path.join(os.path.dirname(__file__), "static", "php-lsp") + + # Verify both node and npm are installed + is_node_installed = shutil.which('node') is not None + assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." + is_npm_installed = shutil.which('npm') is not None + assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + + # Install intelephense if not already installed + if not os.path.exists(intelephense_ls_dir): + os.makedirs(intelephense_ls_dir, exist_ok=True) + for dependency in runtime_dependencies: + # Windows doesn't support the 'user' parameter and doesn't have pwd module + if PlatformUtils.get_platform_id().value.startswith("win"): + subprocess.run( + dependency["command"], + shell=True, + check=True, + cwd=intelephense_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + else: + # On Unix-like systems, run as non-root user + import pwd + user = pwd.getpwuid(os.getuid()).pw_name + subprocess.run( + dependency["command"], + shell=True, + check=True, + user=user, + cwd=intelephense_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + intelephense_executable_path = os.path.join(intelephense_ls_dir, "node_modules", ".bin", "intelephense") + assert os.path.exists(intelephense_executable_path), "intelephense executable not found. Please install intelephense and try again." + + return f"{intelephense_executable_path} --stdio" + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + # Setup runtime dependencies before initializing + intelephense_cmd = self.setup_runtime_dependencies(logger, config) + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path), + "php" + ) + self.request_id = 0 + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the TypeScript Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """Start Intelephense server process""" + def register_capability_handler(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + def do_nothing(params): + return + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + self.logger.log("Starting Intelephense server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.logger.log( + "After sent initialize params", + logging.INFO, + ) + + # Verify server capabilities + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + self.server.notify.initialized({}) + self.completions_available.set() + + # Intelephense server is typically ready immediately after initialization + # TODO: This is probably incorrect; the server does send an initialized notification, which we could wait for! + + @override + # For some reason, the LS may need longer to process this, so we just retry + def _send_references_request(self, relative_file_path: str, line: int, column: int): + # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is + # despite the LS having processed requests already. I don't know what causes this, but sleeping + # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. + # May be related to the time it takes to read the files or something like that. + # The sleeping doesn't seem to be needed on all systems + sleep(1) + return super()._send_references_request(relative_file_path, line, column) + + @override + def _send_definition_request(self, definition_params: DefinitionParams): + # TODO: same as above, also only a problem if the definition is in another file + sleep(1) + return super()._send_definition_request(definition_params) diff --git a/src/solidlsp/language_servers/intelephense/runtime_dependencies.json b/src/solidlsp/language_servers/intelephense/runtime_dependencies.json new file mode 100644 index 00000000..e3a6d7e1 --- /dev/null +++ b/src/solidlsp/language_servers/intelephense/runtime_dependencies.json @@ -0,0 +1,10 @@ +{ + "_description": "Used to download the runtime dependencies for running intelephense. Obtained from https://www.npmjs.com/package/intelephense", + "runtimeDependencies": [ + { + "id": "intelephense", + "description": "Intelephense package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", + "command": "npm install --prefix ./ intelephense@1.14.4" + } + ] +} diff --git a/src/solidlsp/language_servers/jedi_language_server/initialize_params.json b/src/solidlsp/language_servers/jedi_language_server/initialize_params.json new file mode 100644 index 00000000..5a04872c --- /dev/null +++ b/src/solidlsp/language_servers/jedi_language_server/initialize_params.json @@ -0,0 +1,905 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.81.0-insider" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": true, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span" + ] + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + }, + "experimental": { + "snippetTextEdit": true, + "codeActionGroup": true, + "hoverActions": true, + "serverStatusNotification": true, + "colorDiagnosticOutput": true, + "openServerLogs": true, + "commands": { + "commands": [ + "rust-analyzer.runSingle", + "rust-analyzer.debugSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "editor.action.triggerParameterHints" + ] + } + } + }, + "initializationOptions": { + "cargoRunner": null, + "runnables": { + "extraEnv": null, + "problemMatcher": [ + "$rustc" + ], + "command": null, + "extraArgs": [] + }, + "server": { + "path": null, + "extraEnv": null + }, + "trace": { + "server": "verbose", + "extension": true + }, + "debug": { + "engine": "auto", + "sourceFileMap": { + "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" + }, + "openDebugPane": false, + "engineSettings": {} + }, + "restartServerOnConfigChange": false, + "typing": { + "continueCommentsOnNewline": true, + "autoClosingAngleBrackets": { + "enable": false + } + }, + "diagnostics": { + "previewRustcOutput": false, + "useRustcErrorCode": false, + "disabled": [], + "enable": true, + "experimental": { + "enable": false + }, + "remapPrefix": {}, + "warningsAsHint": [], + "warningsAsInfo": [] + }, + "discoverProjectCommand": null, + "showUnlinkedFileNotification": true, + "showDependenciesExplorer": true, + "assist": { + "emitMustUse": false, + "expressionFillDefault": "todo" + }, + "cachePriming": { + "enable": true, + "numThreads": 0 + }, + "cargo": { + "autoreload": true, + "buildScripts": { + "enable": true, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "overrideCommand": null, + "useRustcWrapper": true + }, + "cfgs": {}, + "extraArgs": [], + "extraEnv": {}, + "features": [], + "noDefaultFeatures": false, + "sysroot": "discover", + "sysrootSrc": null, + "target": null, + "unsetTest": [ + "core" + ] + }, + "checkOnSave": true, + "check": { + "allTargets": true, + "command": "check", + "extraArgs": [], + "extraEnv": {}, + "features": null, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "noDefaultFeatures": null, + "overrideCommand": null, + "targets": null + }, + "completion": { + "autoimport": { + "enable": true + }, + "autoself": { + "enable": true + }, + "callable": { + "snippets": "fill_arguments" + }, + "limit": null, + "postfix": { + "enable": true + }, + "privateEditable": { + "enable": false + }, + "snippets": { + "custom": { + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr" + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr" + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr" + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr" + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr" + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr" + } + } + } + }, + "files": { + "excludeDirs": [], + "watcher": "client" + }, + "highlightRelated": { + "breakPoints": { + "enable": true + }, + "closureCaptures": { + "enable": true + }, + "exitPoints": { + "enable": true + }, + "references": { + "enable": true + }, + "yieldPoints": { + "enable": true + } + }, + "hover": { + "actions": { + "debug": { + "enable": true + }, + "enable": true, + "gotoTypeDef": { + "enable": true + }, + "implementations": { + "enable": true + }, + "references": { + "enable": false + }, + "run": { + "enable": true + } + }, + "documentation": { + "enable": true, + "keywords": { + "enable": true + } + }, + "links": { + "enable": true + }, + "memoryLayout": { + "alignment": "hexadecimal", + "enable": true, + "niches": false, + "offset": "hexadecimal", + "size": "both" + } + }, + "imports": { + "granularity": { + "enforce": false, + "group": "crate" + }, + "group": { + "enable": true + }, + "merge": { + "glob": true + }, + "prefer": { + "no": { + "std": false + } + }, + "prefix": "plain" + }, + "inlayHints": { + "bindingModeHints": { + "enable": false + }, + "chainingHints": { + "enable": true + }, + "closingBraceHints": { + "enable": true, + "minLines": 25 + }, + "closureCaptureHints": { + "enable": false + }, + "closureReturnTypeHints": { + "enable": "never" + }, + "closureStyle": "impl_fn", + "discriminantHints": { + "enable": "never" + }, + "expressionAdjustmentHints": { + "enable": "never", + "hideOutsideUnsafe": false, + "mode": "prefix" + }, + "lifetimeElisionHints": { + "enable": "never", + "useParameterNames": false + }, + "maxLength": 25, + "parameterHints": { + "enable": true + }, + "reborrowHints": { + "enable": "never" + }, + "renderColons": true, + "typeHints": { + "enable": true, + "hideClosureInitialization": false, + "hideNamedConstructor": false + } + }, + "interpret": { + "tests": false + }, + "joinLines": { + "joinAssignments": true, + "joinElseIf": true, + "removeTrailingComma": true, + "unwrapTrivialBlock": true + }, + "lens": { + "debug": { + "enable": true + }, + "enable": true, + "forceCustomCommands": true, + "implementations": { + "enable": true + }, + "location": "above_name", + "references": { + "adt": { + "enable": false + }, + "enumVariant": { + "enable": false + }, + "method": { + "enable": false + }, + "trait": { + "enable": false + } + }, + "run": { + "enable": true + } + }, + "linkedProjects": [], + "lru": { + "capacity": null, + "query": { + "capacities": {} + } + }, + "notifications": { + "cargoTomlNotFound": true + }, + "numThreads": null, + "procMacro": { + "attributes": { + "enable": true + }, + "enable": true, + "ignored": {}, + "server": null + }, + "references": { + "excludeImports": false + }, + "rustc": { + "source": null + }, + "rustfmt": { + "extraArgs": [], + "overrideCommand": null, + "rangeFormatting": { + "enable": false + } + }, + "semanticHighlighting": { + "doc": { + "comment": { + "inject": { + "enable": true + } + } + }, + "nonStandardTokens": true, + "operator": { + "enable": true, + "specialization": { + "enable": false + } + }, + "punctuation": { + "enable": false, + "separate": { + "macro": { + "bang": false + } + }, + "specialization": { + "enable": false + } + }, + "strings": { + "enable": true + } + }, + "signatureInfo": { + "detail": "full", + "documentation": { + "enable": true + } + }, + "workspace": { + "symbols": { + "maxSymbols": 0 + } + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/jedi_language_server/jedi_server.py b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py new file mode 100644 index 00000000..9a4dbc8a --- /dev/null +++ b/src/solidlsp/language_servers/jedi_language_server/jedi_server.py @@ -0,0 +1,107 @@ +""" +Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. +""" + +import json +import logging +import os +import pathlib + +from overrides import override + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.ls import SolidLanguageServer + + +class JediServer(SolidLanguageServer): + """ + Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a JediServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd="jedi-language-server", cwd=repository_root_path), + "python", + ) + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Jedi Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """ + Starts the JEDI Language Server + """ + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def check_experimental_status(params): + if params["quiescent"] == True: + self.completions_available.set() + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting jedi-language-server server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": [".", "'", '"'], + "resolveProvider": True, + } + + self.server.notify.initialized({}) diff --git a/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json b/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json new file mode 100644 index 00000000..a7b2ec0a --- /dev/null +++ b/src/solidlsp/language_servers/kotlin_language_server/initialize_params.json @@ -0,0 +1,521 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Multilspy Kotlin Client", + "version": "1.0.0" + }, + "locale": "en", + "rootPath": "repository_absolute_path", + "rootUri": "pathlib.Path(repository_absolute_path).as_uri()", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "configuration": true, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": false, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": false, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": true + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0" + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + } + }, + "initializationOptions": { + "workspaceFolders": "[pathlib.Path(repository_absolute_path).as_uri()]", + "storagePath": null, + "codegen": { + "enabled": false + }, + "compiler": { + "jvm": { + "target": "default" + } + }, + "completion": { + "snippets": { + "enabled": true + } + }, + "diagnostics": { + "enabled": true, + "level": 4, + "debounceTime": 250 + }, + "scripts": { + "enabled": true, + "buildScriptsEnabled": true + }, + "indexing": { + "enabled": true + }, + "externalSources": { + "useKlsScheme": false, + "autoConvertToKotlin": false + }, + "inlayHints": { + "typeHints": false, + "parameterHints": false, + "chainedHints": false + }, + "formatting": { + "formatter": "ktfmt", + "ktfmt": { + "style": "google", + "indent": 4, + "maxWidth": 100, + "continuationIndent": 8, + "removeUnusedImports": true + } + } + }, + "trace": "verbose", + "workspaceFolders": "[\n {\n \"uri\": pathlib.Path(repository_absolute_path).as_uri(),\n \"name\": os.path.basename(repository_absolute_path),\n }\n ]" +} diff --git a/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py new file mode 100644 index 00000000..88e71518 --- /dev/null +++ b/src/solidlsp/language_servers/kotlin_language_server/kotlin_language_server.py @@ -0,0 +1,210 @@ +""" +Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. +""" + +import dataclasses +import json +import logging +import os +import pathlib +import stat + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer + + +@dataclasses.dataclass +class KotlinRuntimeDependencyPaths: + """ + Stores the paths to the runtime dependencies of Kotlin Language Server + """ + java_path: str + java_home_path: str + kotlin_executable_path: str + + +class KotlinLanguageServer(SolidLanguageServer): + """ + Provides Kotlin specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Kotlin. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a Kotlin Language Server instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + runtime_dependency_paths = self.setup_runtime_dependencies(logger, config) + self.runtime_dependency_paths = runtime_dependency_paths + + # Create command to execute the Kotlin Language Server script + cmd = f'"{self.runtime_dependency_paths.kotlin_executable_path}"' + + # Set environment variables including JAVA_HOME + proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), + "kotlin", + ) + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> KotlinRuntimeDependencyPaths: + """ + Setup runtime dependencies for Kotlin Language Server. + """ + platform_id = PlatformUtils.get_platform_id() + + # Verify platform support + assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), "Only Windows, Linux and macOS platforms are supported for Kotlin in multilspy at the moment" + + # Load dependency information + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + kotlin_dependency = d["runtimeDependency"] + java_dependency = d["java"][platform_id.value] + + # Setup paths for dependencies + static_dir = os.path.join(os.path.dirname(__file__), "static") + os.makedirs(static_dir, exist_ok=True) + + # Setup Java paths + java_dir = os.path.join(static_dir, "java") + os.makedirs(java_dir, exist_ok=True) + + java_home_path = os.path.join(java_dir, java_dependency["java_home_path"]) + java_path = os.path.join(java_dir, java_dependency["java_path"]) + + # Download and extract Java if not exists + if not os.path.exists(java_path): + logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO) + FileUtils.download_and_extract_archive( + logger, java_dependency["url"], java_dir, java_dependency["archiveType"] + ) + # Make Java executable + if not platform_id.value.startswith("win-"): + os.chmod(java_path, 0o755) + + assert os.path.exists(java_path), f"Java executable not found at {java_path}" + + # Setup Kotlin Language Server paths + kotlin_ls_dir = os.path.join(static_dir, "server") + + # Get platform-specific executable script path + if platform_id.value.startswith("win-"): + kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server.bat") + else: + kotlin_script = os.path.join(kotlin_ls_dir, "bin", "kotlin-language-server") + + # Download and extract Kotlin Language Server if script doesn't exist + if not os.path.exists(kotlin_script): + logger.log("Downloading Kotlin Language Server...", logging.INFO) + FileUtils.download_and_extract_archive( + logger, kotlin_dependency["url"], static_dir, kotlin_dependency["archiveType"] + ) + + # Make script executable on Unix platforms + if os.path.exists(kotlin_script) and not platform_id.value.startswith("win-"): + os.chmod(kotlin_script, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) + + # Use script file + if os.path.exists(kotlin_script): + kotlin_executable_path = kotlin_script + logger.log(f"Using Kotlin Language Server script at {kotlin_script}", logging.INFO) + else: + raise FileNotFoundError(f"Kotlin Language Server script not found at {kotlin_script}") + + return KotlinRuntimeDependencyPaths( + java_path=java_path, + java_home_path=java_home_path, + kotlin_executable_path=kotlin_executable_path + ) + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Kotlin Language Server. + """ + with open(str(pathlib.PurePath(os.path.dirname(__file__), "initialize_params.json")), "r", encoding="utf-8") as f: + d: InitializeParams = json.load(f) + + del d["_description"] + + if not os.path.isabs(repository_absolute_path): + repository_absolute_path = os.path.abspath(repository_absolute_path) + + assert d["processId"] == "os.getpid()" + d["processId"] = os.getpid() + + assert d["rootPath"] == "repository_absolute_path" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "pathlib.Path(repository_absolute_path).as_uri()" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["initializationOptions"]["workspaceFolders"] == "[pathlib.Path(repository_absolute_path).as_uri()]" + d["initializationOptions"]["workspaceFolders"] = [pathlib.Path(repository_absolute_path).as_uri()] + + assert ( + d["workspaceFolders"] + == '[\n {\n "uri": pathlib.Path(repository_absolute_path).as_uri(),\n "name": os.path.basename(repository_absolute_path),\n }\n ]' + ) + d["workspaceFolders"] = [ + { + "uri": pathlib.Path(repository_absolute_path).as_uri(), + "name": os.path.basename(repository_absolute_path), + } + ] + + return d + + def _start_server(self): + """ + Starts the Kotlin Language Server + """ + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + + self.logger.log("Starting Kotlin server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + + capabilities = init_response["capabilities"] + assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" + assert "hoverProvider" in capabilities, "Server must support hover" + assert "completionProvider" in capabilities, "Server must support code completion" + assert "signatureHelpProvider" in capabilities, "Server must support signature help" + assert "definitionProvider" in capabilities, "Server must support go to definition" + assert "referencesProvider" in capabilities, "Server must support find references" + assert "documentSymbolProvider" in capabilities, "Server must support document symbols" + assert "workspaceSymbolProvider" in capabilities, "Server must support workspace symbols" + assert "semanticTokensProvider" in capabilities, "Server must support semantic tokens" + + self.server.notify.initialized({}) + self.completions_available.set() diff --git a/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json new file mode 100644 index 00000000..3568c9d4 --- /dev/null +++ b/src/solidlsp/language_servers/kotlin_language_server/runtime_dependencies.json @@ -0,0 +1,41 @@ +{ + "_description": "Used to download the runtime dependencies for Kotlin Language Server from https://github.com/fwcd/kotlin-language-server", + "runtimeDependency": { + "id": "KotlinLsp", + "description": "Kotlin Language Server", + "url": "https://github.com/fwcd/kotlin-language-server/releases/download/1.3.13/server.zip", + "archiveType": "zip" + }, + "java": { + "win-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-win32-x86_64", + "java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe" + }, + "linux-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-linux-x86_64", + "java_path": "extension/jre/21.0.7-linux-x86_64/bin/java" + }, + "linux-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-linux-aarch64", + "java_path": "extension/jre/21.0.7-linux-aarch64/bin/java" + }, + "osx-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-macosx-x86_64", + "java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java" + }, + "osx-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-macosx-aarch64", + "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java" + } + } +} diff --git a/src/solidlsp/language_servers/omnisharp/initialize_params.json b/src/solidlsp/language_servers/omnisharp/initialize_params.json new file mode 100644 index 00000000..f9ee86c3 --- /dev/null +++ b/src/solidlsp/language_servers/omnisharp/initialize_params.json @@ -0,0 +1,631 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.82.0-insider" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": false, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span" + ] + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + }, + "experimental": { + "snippetTextEdit": true, + "codeActionGroup": true, + "hoverActions": true, + "serverStatusNotification": true, + "colorDiagnosticOutput": true, + "openServerLogs": true, + "commands": { + "commands": [ + "editor.action.triggerParameterHints" + ] + } + } + }, + "initializationOptions": { + "RoslynExtensionsOptions": { + "EnableDecompilationSupport": false, + "EnableAnalyzersSupport": true, + "EnableImportCompletion": true, + "EnableAsyncCompletion": false, + "DocumentAnalysisTimeoutMs": 30000, + "DiagnosticWorkersThreadCount": 18, + "AnalyzeOpenDocumentsOnly": true, + "InlayHintsOptions": { + "EnableForParameters": false, + "ForLiteralParameters": false, + "ForIndexerParameters": false, + "ForObjectCreationParameters": false, + "ForOtherParameters": false, + "SuppressForParametersThatDifferOnlyBySuffix": false, + "SuppressForParametersThatMatchMethodIntent": false, + "SuppressForParametersThatMatchArgumentName": false, + "EnableForTypes": false, + "ForImplicitVariableTypes": false, + "ForLambdaParameterTypes": false, + "ForImplicitObjectCreation": false + }, + "LocationPaths": null + }, + "FormattingOptions": { + "OrganizeImports": false, + "EnableEditorConfigSupport": true, + "NewLine": "\n", + "UseTabs": false, + "TabSize": 4, + "IndentationSize": 4, + "SpacingAfterMethodDeclarationName": false, + "SeparateImportDirectiveGroups": false, + "SpaceWithinMethodDeclarationParenthesis": false, + "SpaceBetweenEmptyMethodDeclarationParentheses": false, + "SpaceAfterMethodCallName": false, + "SpaceWithinMethodCallParentheses": false, + "SpaceBetweenEmptyMethodCallParentheses": false, + "SpaceAfterControlFlowStatementKeyword": true, + "SpaceWithinExpressionParentheses": false, + "SpaceWithinCastParentheses": false, + "SpaceWithinOtherParentheses": false, + "SpaceAfterCast": false, + "SpaceBeforeOpenSquareBracket": false, + "SpaceBetweenEmptySquareBrackets": false, + "SpaceWithinSquareBrackets": false, + "SpaceAfterColonInBaseTypeDeclaration": true, + "SpaceAfterComma": true, + "SpaceAfterDot": false, + "SpaceAfterSemicolonsInForStatement": true, + "SpaceBeforeColonInBaseTypeDeclaration": true, + "SpaceBeforeComma": false, + "SpaceBeforeDot": false, + "SpaceBeforeSemicolonsInForStatement": false, + "SpacingAroundBinaryOperator": "single", + "IndentBraces": false, + "IndentBlock": true, + "IndentSwitchSection": true, + "IndentSwitchCaseSection": true, + "IndentSwitchCaseSectionWhenBlock": true, + "LabelPositioning": "oneLess", + "WrappingPreserveSingleLine": true, + "WrappingKeepStatementsOnSingleLine": true, + "NewLinesForBracesInTypes": true, + "NewLinesForBracesInMethods": true, + "NewLinesForBracesInProperties": true, + "NewLinesForBracesInAccessors": true, + "NewLinesForBracesInAnonymousMethods": true, + "NewLinesForBracesInControlBlocks": true, + "NewLinesForBracesInAnonymousTypes": true, + "NewLinesForBracesInObjectCollectionArrayInitializers": true, + "NewLinesForBracesInLambdaExpressionBody": true, + "NewLineForElse": true, + "NewLineForCatch": true, + "NewLineForFinally": true, + "NewLineForMembersInObjectInit": true, + "NewLineForMembersInAnonymousTypes": true, + "NewLineForClausesInQuery": true + }, + "FileOptions": { + "SystemExcludeSearchPatterns": [ + "**/node_modules/**/*", + "**/bin/**/*", + "**/obj/**/*", + "**/.git/**/*", + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store", + "**/Thumbs.db" + ], + "ExcludeSearchPatterns": [] + }, + "RenameOptions": { + "RenameOverloads": false, + "RenameInStrings": false, + "RenameInComments": false + }, + "ImplementTypeOptions": { + "InsertionBehavior": 0, + "PropertyGenerationBehavior": 0 + }, + "DotNetCliOptions": { + "LocationPaths": null + }, + "Plugins": { + "LocationPaths": null + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/omnisharp/omnisharp.py b/src/solidlsp/language_servers/omnisharp/omnisharp.py new file mode 100644 index 00000000..a449a985 --- /dev/null +++ b/src/solidlsp/language_servers/omnisharp/omnisharp.py @@ -0,0 +1,393 @@ +""" +Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. +""" + +import json +import logging +import os +import pathlib +import stat +import threading +from typing import Iterable + +from overrides import override + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_exceptions import MultilspyException +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import FileUtils, PlatformUtils, PlatformId, DotnetVersion +from solidlsp.ls import SolidLanguageServer + + +def breadth_first_file_scan(root) -> Iterable[str]: + """ + This function was obtained from https://stackoverflow.com/questions/49654234/is-there-a-breadth-first-search-option-available-in-os-walk-or-equivalent-py + It traverses the directory tree in breadth first order. + """ + dirs = [root] + # while we has dirs to scan + while len(dirs): + next_dirs = [] + for parent in dirs: + # scan each dir + for f in os.listdir(parent): + # if there is a dir, then save for next ittr + # if it is a file then yield it (we'll return later) + ff = os.path.join(parent, f) + if os.path.isdir(ff): + next_dirs.append(ff) + else: + yield ff + + # once we've done all the current dirs then + # we set up the next itter as the child dirs + # from the current itter. + dirs = next_dirs + + +def find_least_depth_sln_file(root_dir) -> str | None: + for filename in breadth_first_file_scan(root_dir): + if filename.endswith(".sln"): + return filename + return None + + +class OmniSharp(SolidLanguageServer): + """ + Provides C# specific instantiation of the LanguageServer class. Contains various configurations and settings specific to C#. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates an OmniSharp instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + omnisharp_executable_path, dll_path = self.setupRuntimeDependencies(logger, config) + + slnfilename = find_least_depth_sln_file(repository_root_path) + if slnfilename is None: + logger.log("No *.sln file found in repository", logging.ERROR) + raise MultilspyException("No SLN file found in repository") + + cmd = " ".join( + [ + omnisharp_executable_path, + "-lsp", + "--encoding", + "ascii", + "-z", + "-s", + f'"{slnfilename}"', + "--hostPID", + str(os.getpid()), + "DotNet:enablePackageRestore=false", + "--loglevel", + "trace", + "--plugin", + dll_path, + "FileOptions:SystemExcludeSearchPatterns:0=**/.git", + "FileOptions:SystemExcludeSearchPatterns:1=**/.svn", + "FileOptions:SystemExcludeSearchPatterns:2=**/.hg", + "FileOptions:SystemExcludeSearchPatterns:3=**/CVS", + "FileOptions:SystemExcludeSearchPatterns:4=**/.DS_Store", + "FileOptions:SystemExcludeSearchPatterns:5=**/Thumbs.db", + "RoslynExtensionsOptions:EnableAnalyzersSupport=true", + "FormattingOptions:EnableEditorConfigSupport=true", + "RoslynExtensionsOptions:EnableImportCompletion=true", + "Sdk:IncludePrereleases=true", + "RoslynExtensionsOptions:AnalyzeOpenDocumentsOnly=true", + "formattingOptions:useTabs=false", + "formattingOptions:tabSize=4", + "formattingOptions:indentationSize=4", + ] + ) + super().__init__( + config, logger, repository_root_path, ProcessLaunchInfo(cmd=cmd, cwd=repository_root_path), "csharp" + ) + + self.server_ready = threading.Event() + self.definition_available = threading.Event() + self.references_available = threading.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj"] + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Omnisharp Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def setupRuntimeDependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> tuple[str, str]: + """ + Setup runtime dependencies for OmniSharp. + """ + platform_id = PlatformUtils.get_platform_id() + dotnet_version = PlatformUtils.get_dotnet_version() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + assert platform_id in [ + PlatformId.LINUX_x64, + PlatformId.WIN_x64, + ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" + assert dotnet_version in [ + DotnetVersion.V6, + DotnetVersion.V7, + DotnetVersion.V8 + ], "Only dotnet version 6 and 7 are supported in multilspy at the moment" + + # TODO: Do away with this assumption + # Currently, runtime binaries are not available for .Net 7 and .Net 8. Hence, we assume .Net 6 runtime binaries to be compatible with .Net 7, .Net 8 + if dotnet_version in [DotnetVersion.V7, DotnetVersion.V8]: + dotnet_version = DotnetVersion.V6 + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + runtime_dependencies = [ + dependency + for dependency in runtime_dependencies + if not ("dotnet_version" in dependency) or dependency["dotnet_version"] == dotnet_version.value + ] + assert len(runtime_dependencies) == 2 + runtime_dependencies = { + runtime_dependencies[0]["id"]: runtime_dependencies[0], + runtime_dependencies[1]["id"]: runtime_dependencies[1], + } + + assert "OmniSharp" in runtime_dependencies + assert "RazorOmnisharp" in runtime_dependencies + + omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "OmniSharp") + if not os.path.exists(omnisharp_ls_dir): + os.makedirs(omnisharp_ls_dir) + FileUtils.download_and_extract_archive( + logger, runtime_dependencies["OmniSharp"]["url"], omnisharp_ls_dir, "zip" + ) + omnisharp_executable_path = os.path.join(omnisharp_ls_dir, runtime_dependencies["OmniSharp"]["binaryName"]) + assert os.path.exists(omnisharp_executable_path) + os.chmod(omnisharp_executable_path, stat.S_IEXEC) + + razor_omnisharp_ls_dir = os.path.join(os.path.dirname(__file__), "static", "RazorOmnisharp") + if not os.path.exists(razor_omnisharp_ls_dir): + os.makedirs(razor_omnisharp_ls_dir) + FileUtils.download_and_extract_archive( + logger, runtime_dependencies["RazorOmnisharp"]["url"], razor_omnisharp_ls_dir, "zip" + ) + razor_omnisharp_dll_path = os.path.join( + razor_omnisharp_ls_dir, runtime_dependencies["RazorOmnisharp"]["dll_path"] + ) + assert os.path.exists(razor_omnisharp_dll_path) + + return omnisharp_executable_path, razor_omnisharp_dll_path + + def _start_server(self): + """ + Starts the Omnisharp Language Server + """ + + def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "textDocument/definition": + self.definition_available.set() + if registration["method"] == "textDocument/references": + self.references_available.set() + if registration["method"] == "textDocument/completion": + self.completions_available.set() + + def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + # if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + # self.service_ready_event.set() + pass + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + def workspace_configuration_handler(params): + # TODO: We do not know the appropriate way to handle this request. Should ideally contact the OmniSharp dev team + return [ + { + "RoslynExtensionsOptions": { + "EnableDecompilationSupport": False, + "EnableAnalyzersSupport": True, + "EnableImportCompletion": True, + "EnableAsyncCompletion": False, + "DocumentAnalysisTimeoutMs": 30000, + "DiagnosticWorkersThreadCount": 18, + "AnalyzeOpenDocumentsOnly": True, + "InlayHintsOptions": { + "EnableForParameters": False, + "ForLiteralParameters": False, + "ForIndexerParameters": False, + "ForObjectCreationParameters": False, + "ForOtherParameters": False, + "SuppressForParametersThatDifferOnlyBySuffix": False, + "SuppressForParametersThatMatchMethodIntent": False, + "SuppressForParametersThatMatchArgumentName": False, + "EnableForTypes": False, + "ForImplicitVariableTypes": False, + "ForLambdaParameterTypes": False, + "ForImplicitObjectCreation": False, + }, + "LocationPaths": None, + }, + "FormattingOptions": { + "OrganizeImports": False, + "EnableEditorConfigSupport": True, + "NewLine": "\n", + "UseTabs": False, + "TabSize": 4, + "IndentationSize": 4, + "SpacingAfterMethodDeclarationName": False, + "SeparateImportDirectiveGroups": False, + "SpaceWithinMethodDeclarationParenthesis": False, + "SpaceBetweenEmptyMethodDeclarationParentheses": False, + "SpaceAfterMethodCallName": False, + "SpaceWithinMethodCallParentheses": False, + "SpaceBetweenEmptyMethodCallParentheses": False, + "SpaceAfterControlFlowStatementKeyword": True, + "SpaceWithinExpressionParentheses": False, + "SpaceWithinCastParentheses": False, + "SpaceWithinOtherParentheses": False, + "SpaceAfterCast": False, + "SpaceBeforeOpenSquareBracket": False, + "SpaceBetweenEmptySquareBrackets": False, + "SpaceWithinSquareBrackets": False, + "SpaceAfterColonInBaseTypeDeclaration": True, + "SpaceAfterComma": True, + "SpaceAfterDot": False, + "SpaceAfterSemicolonsInForStatement": True, + "SpaceBeforeColonInBaseTypeDeclaration": True, + "SpaceBeforeComma": False, + "SpaceBeforeDot": False, + "SpaceBeforeSemicolonsInForStatement": False, + "SpacingAroundBinaryOperator": "single", + "IndentBraces": False, + "IndentBlock": True, + "IndentSwitchSection": True, + "IndentSwitchCaseSection": True, + "IndentSwitchCaseSectionWhenBlock": True, + "LabelPositioning": "oneLess", + "WrappingPreserveSingleLine": True, + "WrappingKeepStatementsOnSingleLine": True, + "NewLinesForBracesInTypes": True, + "NewLinesForBracesInMethods": True, + "NewLinesForBracesInProperties": True, + "NewLinesForBracesInAccessors": True, + "NewLinesForBracesInAnonymousMethods": True, + "NewLinesForBracesInControlBlocks": True, + "NewLinesForBracesInAnonymousTypes": True, + "NewLinesForBracesInObjectCollectionArrayInitializers": True, + "NewLinesForBracesInLambdaExpressionBody": True, + "NewLineForElse": True, + "NewLineForCatch": True, + "NewLineForFinally": True, + "NewLineForMembersInObjectInit": True, + "NewLineForMembersInAnonymousTypes": True, + "NewLineForClausesInQuery": True, + }, + "FileOptions": { + "SystemExcludeSearchPatterns": [ + "**/node_modules/**/*", + "**/bin/**/*", + "**/obj/**/*", + "**/.git/**/*", + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store", + "**/Thumbs.db", + ], + "ExcludeSearchPatterns": [], + }, + "RenameOptions": { + "RenameOverloads": False, + "RenameInStrings": False, + "RenameInComments": False, + }, + "ImplementTypeOptions": { + "InsertionBehavior": 0, + "PropertyGenerationBehavior": 0, + }, + "DotNetCliOptions": {"LocationPaths": None}, + "Plugins": {"LocationPaths": None}, + } + ] + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + self.server.on_request("workspace/configuration", workspace_configuration_handler) + + self.logger.log("Starting OmniSharp server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.server.notify.initialized({}) + with open(os.path.join(os.path.dirname(__file__), "workspace_did_change_configuration.json"), "r", encoding="utf-8") as f: + self.server.notify.workspace_did_change_configuration({ + "settings": json.load(f) + }) + assert "capabilities" in init_response + if ( + "definitionProvider" in init_response["capabilities"] + and init_response["capabilities"]["definitionProvider"] + ): + self.definition_available.set() + if ( + "referencesProvider" in init_response["capabilities"] + and init_response["capabilities"]["referencesProvider"] + ): + self.references_available.set() + + self.definition_available.wait() + self.references_available.wait() diff --git a/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json b/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json new file mode 100644 index 00000000..7dfa1fd4 --- /dev/null +++ b/src/solidlsp/language_servers/omnisharp/runtime_dependencies.json @@ -0,0 +1,441 @@ +{ + "_description": "Used to download the runtime dependencies for running OmniSharp. Obtained from https://github.com/dotnet/vscode-csharp/blob/main/package.json", + "runtimeDependencies": [ + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 4 / x86)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x86-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "win32" + ], + "architectures": [ + "x86" + ], + "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", + "platformId": "win-x86", + "isFramework": true, + "integrity": "C81CE2099AD494EF63F9D88FAA70D55A68CF175810F944526FF94AAC7A5109F9", + "dotnet_version": "4", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 6 / x86)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x86-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "win32" + ], + "architectures": [ + "x86" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "win-x86", + "isFramework": false, + "integrity": "B7E62415CFC3DAC2154AC636C5BF0FB4B2C9BBF11B5A1FBF72381DDDED59791E", + "dotnet_version": "6", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 4 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x64-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "win32" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", + "platformId": "win-x64", + "isFramework": true, + "integrity": "BE0ED10AACEA17E14B78BD0D887DE5935D4ECA3712192A701F3F2100CA3C8B6E", + "dotnet_version": "4", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 6 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-x64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "win32" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "win-x64", + "isFramework": false, + "integrity": "A73327395E7EF92C1D8E307055463DA412662C03F077ECC743462FD2760BB537", + "dotnet_version": "6", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 4 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-arm64-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "win32" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10/OmniSharp.exe", + "platformId": "win-arm64", + "isFramework": true, + "integrity": "32FA0067B0639F87760CD1A769B16E6A53588C137C4D31661836CA4FB28D3DD6", + "dotnet_version": "4", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Windows (.NET 6 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-win-arm64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "win32" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "win-arm64", + "isFramework": false, + "integrity": "433F9B360CAA7B4DDD85C604D5C5542C1A718BCF2E71B2BCFC7526E6D41F4E8F", + "dotnet_version": "6", + "binaryName": "OmniSharp.exe" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for OSX (Mono / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "darwin" + ], + "architectures": [ + "x86_64", + "arm64" + ], + "binaries": [ + "./mono.osx", + "./run" + ], + "installTestPath": "./.omnisharp/1.39.10/run", + "platformId": "osx", + "isFramework": true, + "integrity": "2CC42F0EC7C30CFA8858501D12ECB6FB685A1FCFB8ECB35698A4B12406551968", + "dotnet_version": "mono" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for OSX (.NET 6 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-x64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "darwin" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "osx-x64", + "isFramework": false, + "integrity": "C9D6E9F2C839A66A7283AE6A9EC545EE049B48EB230D33E91A6322CB67FF9D97", + "dotnet_version": "6" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for OSX (.NET 6 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-osx-arm64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "darwin" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "osx-arm64", + "isFramework": false, + "integrity": "851350F52F83E3BAD5A92D113E4B9882FCD1DEB16AA84FF94B6F2CEE3C70051E", + "dotnet_version": "6" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (Mono / x86)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x86-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "linux" + ], + "architectures": [ + "x86", + "i686" + ], + "binaries": [ + "./mono.linux-x86", + "./run" + ], + "installTestPath": "./.omnisharp/1.39.10/run", + "platformId": "linux-x86", + "isFramework": true, + "integrity": "474B1CDBAE64CFEC655FB6B0659BCE481023C48274441C72991E67B6E13E56A1", + "dotnet_version": "mono" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (Mono / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x64-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "linux" + ], + "architectures": [ + "x86_64" + ], + "binaries": [ + "./mono.linux-x86_64", + "./run" + ], + "installTestPath": "./.omnisharp/1.39.10/run", + "platformId": "linux-x64", + "isFramework": true, + "integrity": "FB4CAA47343265100349375D79DBCCE1868950CED675CB07FCBE8462EDBCDD37", + "dotnet_version": "mono" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (.NET 6 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-x64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "linux" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "linux-x64", + "isFramework": false, + "integrity": "0926D3BEA060BF4373356B2FC0A68C10D0DE1B1150100B551BA5932814CE51E2", + "dotnet_version": "6", + "binaryName": "OmniSharp" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (Mono / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-arm64-1.39.10.zip", + "installPath": ".omnisharp/1.39.10", + "platforms": [ + "linux" + ], + "architectures": [ + "arm64" + ], + "binaries": [ + "./mono.linux-arm64", + "./run" + ], + "installTestPath": "./.omnisharp/1.39.10/run", + "platformId": "linux-arm64", + "isFramework": true, + "integrity": "478F3594DFD0167E9A56E36F0364A86C73F8132A3E7EA916CA1419EFE141D2CC", + "dotnet_version": "mono" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux (.NET 6 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-arm64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "linux" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "linux-arm64", + "isFramework": false, + "integrity": "6FB6A572043A74220A92F6C19C7BB0C3743321C7563A815FD2702EF4FA7D688E", + "dotnet_version": "6" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux musl (.NET 6 / x64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-musl-x64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "linux-musl" + ], + "architectures": [ + "x86_64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "linux-musl-x64", + "isFramework": false, + "integrity": "6BFDA3AD11DBB0C6514B86ECC3E1597CC41C6E309B7575F7C599E07D9E2AE610", + "dotnet_version": "6" + }, + { + "id": "OmniSharp", + "description": "OmniSharp for Linux musl (.NET 6 / arm64)", + "url": "https://roslynomnisharp.blob.core.windows.net/releases/1.39.10/omnisharp-linux-musl-arm64-net6.0-1.39.10.zip", + "installPath": ".omnisharp/1.39.10-net6.0", + "platforms": [ + "linux-musl" + ], + "architectures": [ + "arm64" + ], + "installTestPath": "./.omnisharp/1.39.10-net6.0/OmniSharp.dll", + "platformId": "linux-musl-arm64", + "isFramework": false, + "integrity": "DA63619EA024EB9BBF6DB5A85C6150CAB5C0BD554544A3596ED1B17F926D6875", + "dotnet_version": "6" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Windows / x64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/8d42e62ea4051381c219b3e31bc4eced/razorlanguageserver-win-x64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "win32" + ], + "architectures": [ + "x86_64" + ], + "platformId": "win-x64", + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Windows / x86)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/e440c4f3a4a96334fe177513935fa010/razorlanguageserver-win-x86-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "win32" + ], + "architectures": [ + "x86" + ], + "platformId": "win-x86", + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Windows / ARM64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/4ef26e45cf32fe8d51c0e7dd21f1fef6/razorlanguageserver-win-arm64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "win32" + ], + "architectures": [ + "arm64" + ], + "platformId": "win-arm64", + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Linux / x64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/6d4e23a3c7cf0465743950a39515a716/razorlanguageserver-linux-x64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "linux" + ], + "architectures": [ + "x86_64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "linux-x64", + "dll_path": "OmniSharpPlugin/Microsoft.AspNetCore.Razor.OmniSharpPlugin.dll" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Linux ARM64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/85deebd44647ebf65724cc291d722283/razorlanguageserver-linux-arm64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "linux" + ], + "architectures": [ + "arm64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "linux-arm64" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Linux musl / x64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/4f0caa94ae182785655efb15eafcef23/razorlanguageserver-linux-musl-x64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "linux-musl" + ], + "architectures": [ + "x86_64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "linux-musl-x64" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (Linux musl ARM64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/0a24828206a6f3b4bc743d058ef88ce7/razorlanguageserver-linux-musl-arm64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "linux-musl" + ], + "architectures": [ + "arm64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "linux-musl-arm64" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (macOS / x64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/2afcafaf41082989efcc10405abb9314/razorlanguageserver-osx-x64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "darwin" + ], + "architectures": [ + "x86_64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "osx-x64" + }, + { + "id": "RazorOmnisharp", + "description": "Razor Language Server for OmniSharp (macOS ARM64)", + "url": "https://download.visualstudio.microsoft.com/download/pr/aee63398-023f-48db-bba2-30162c68f0c4/8bf2ed2f00d481a5987e3eb5165afddd/razorlanguageserver-osx-arm64-7.0.0-preview.23363.1.zip", + "installPath": ".razoromnisharp", + "platforms": [ + "darwin" + ], + "architectures": [ + "arm64" + ], + "binaries": [ + "./rzls" + ], + "platformId": "osx-arm64" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json b/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json new file mode 100644 index 00000000..148841c3 --- /dev/null +++ b/src/solidlsp/language_servers/omnisharp/workspace_did_change_configuration.json @@ -0,0 +1,111 @@ +{ + "RoslynExtensionsOptions": { + "EnableDecompilationSupport": false, + "EnableAnalyzersSupport": true, + "EnableImportCompletion": true, + "EnableAsyncCompletion": false, + "DocumentAnalysisTimeoutMs": 30000, + "DiagnosticWorkersThreadCount": 18, + "AnalyzeOpenDocumentsOnly": true, + "InlayHintsOptions": { + "EnableForParameters": false, + "ForLiteralParameters": false, + "ForIndexerParameters": false, + "ForObjectCreationParameters": false, + "ForOtherParameters": false, + "SuppressForParametersThatDifferOnlyBySuffix": false, + "SuppressForParametersThatMatchMethodIntent": false, + "SuppressForParametersThatMatchArgumentName": false, + "EnableForTypes": false, + "ForImplicitVariableTypes": false, + "ForLambdaParameterTypes": false, + "ForImplicitObjectCreation": false + }, + "LocationPaths": null + }, + "FormattingOptions": { + "OrganizeImports": false, + "EnableEditorConfigSupport": true, + "NewLine": "\n", + "UseTabs": false, + "TabSize": 4, + "IndentationSize": 4, + "SpacingAfterMethodDeclarationName": false, + "SeparateImportDirectiveGroups": false, + "SpaceWithinMethodDeclarationParenthesis": false, + "SpaceBetweenEmptyMethodDeclarationParentheses": false, + "SpaceAfterMethodCallName": false, + "SpaceWithinMethodCallParentheses": false, + "SpaceBetweenEmptyMethodCallParentheses": false, + "SpaceAfterControlFlowStatementKeyword": true, + "SpaceWithinExpressionParentheses": false, + "SpaceWithinCastParentheses": false, + "SpaceWithinOtherParentheses": false, + "SpaceAfterCast": false, + "SpaceBeforeOpenSquareBracket": false, + "SpaceBetweenEmptySquareBrackets": false, + "SpaceWithinSquareBrackets": false, + "SpaceAfterColonInBaseTypeDeclaration": true, + "SpaceAfterComma": true, + "SpaceAfterDot": false, + "SpaceAfterSemicolonsInForStatement": true, + "SpaceBeforeColonInBaseTypeDeclaration": true, + "SpaceBeforeComma": false, + "SpaceBeforeDot": false, + "SpaceBeforeSemicolonsInForStatement": false, + "SpacingAroundBinaryOperator": "single", + "IndentBraces": false, + "IndentBlock": true, + "IndentSwitchSection": true, + "IndentSwitchCaseSection": true, + "IndentSwitchCaseSectionWhenBlock": true, + "LabelPositioning": "oneLess", + "WrappingPreserveSingleLine": true, + "WrappingKeepStatementsOnSingleLine": true, + "NewLinesForBracesInTypes": true, + "NewLinesForBracesInMethods": true, + "NewLinesForBracesInProperties": true, + "NewLinesForBracesInAccessors": true, + "NewLinesForBracesInAnonymousMethods": true, + "NewLinesForBracesInControlBlocks": true, + "NewLinesForBracesInAnonymousTypes": true, + "NewLinesForBracesInObjectCollectionArrayInitializers": true, + "NewLinesForBracesInLambdaExpressionBody": true, + "NewLineForElse": true, + "NewLineForCatch": true, + "NewLineForFinally": true, + "NewLineForMembersInObjectInit": true, + "NewLineForMembersInAnonymousTypes": true, + "NewLineForClausesInQuery": true + }, + "FileOptions": { + "SystemExcludeSearchPatterns": [ + "**/node_modules/**/*", + "**/bin/**/*", + "**/obj/**/*", + "**/.git/**/*", + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store", + "**/Thumbs.db" + ], + "ExcludeSearchPatterns": [] + }, + "RenameOptions": { + "RenameOverloads": false, + "RenameInStrings": false, + "RenameInComments": false + }, + "ImplementTypeOptions": { + "InsertionBehavior": 0, + "PropertyGenerationBehavior": 0 + }, + "DotNetCliOptions": { + "LocationPaths": null + }, + "Plugins": { + "LocationPaths": null + } +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py new file mode 100644 index 00000000..b3490502 --- /dev/null +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -0,0 +1,238 @@ +""" +Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. +""" + +import logging +import os +import pathlib +import re +import threading + +from overrides import override + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.ls import SolidLanguageServer + + +class PyrightServer(SolidLanguageServer): + """ + Provides Python specific instantiation of the LanguageServer class using Pyright. + Contains various configurations and settings specific to Python. + """ + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a PyrightServer instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + super().__init__( + config, + logger, + repository_root_path, + # Note 1: we can also use `pyright-langserver --stdio` but it requires pyright to be installed with npm + # Note 2: we can also use `bpyright-langserver --stdio` if we ever are unhappy with pyright + ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path), + "python", + ) + + # Event to signal when initial workspace analysis is complete + self.analysis_complete = threading.Event() + self.found_source_files = False + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Pyright Language Server. + """ + # Create basic initialization parameters + initialize_params: InitializeParams = { # type: ignore + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": pathlib.Path(repository_absolute_path).as_uri(), + "initializationOptions": { + "exclude": [ + "**/__pycache__", + "**/.venv", + "**/.env", + "**/build", + "**/dist", + "**/.pixi", + ], + "reportMissingImports": "error", + }, + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": {"documentChanges": True}, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": {"dynamicRegistration": True}, + "symbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] + }, + }, + "executeCommand": {"dynamicRegistration": True}, + }, + "textDocument": { + "synchronization": {"dynamicRegistration": True, "willSave": True, "willSaveWaitUntil": True, "didSave": True}, + "completion": { + "dynamicRegistration": True, + "contextSupport": True, + "completionItem": { + "snippetSupport": True, + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "deprecatedSupport": True, + "preselectSupport": True, + }, + "completionItemKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] + }, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + }, + }, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": { + "valueSet": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] + }, + "hierarchicalDocumentSymbolSupport": True, + }, + "codeAction": { + "dynamicRegistration": True, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ] + } + }, + }, + "codeLens": {"dynamicRegistration": True}, + "formatting": {"dynamicRegistration": True}, + "rangeFormatting": {"dynamicRegistration": True}, + "onTypeFormatting": {"dynamicRegistration": True}, + "rename": {"dynamicRegistration": True}, + "publishDiagnostics": {"relatedInformation": True}, + }, + }, + "workspaceFolders": [ + {"uri": pathlib.Path(repository_absolute_path).as_uri(), "name": os.path.basename(repository_absolute_path)} + ], + } + + return initialize_params + + def _start_server(self): + """ + Starts the Pyright Language Server and waits for initial workspace analysis to complete. + + This prevents zombie processes by ensuring Pyright has finished its initial background + tasks before we consider the server ready. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and workspace analysis is complete + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown cleanly + ``` + """ + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def window_log_message(msg): + """ + Monitor Pyright's log messages to detect when initial analysis is complete. + Pyright logs "Found X source files" when it finishes scanning the workspace. + """ + message_text = msg.get("message", "") + self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) + + # Look for "Found X source files" which indicates workspace scanning is complete + # Unfortunately, pyright is unreliable and there seems to be no better way + if re.search(r"Found \d+ source files?", message_text): + self.logger.log("Pyright workspace scanning complete", logging.INFO) + self.found_source_files = True + self.analysis_complete.set() + self.completions_available.set() + + def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.logger.log("Received experimental/serverStatus with quiescent=true", logging.INFO) + if not self.found_source_files: + self.analysis_complete.set() + self.completions_available.set() + + # Set up notification handlers + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting pyright-langserver server process", logging.INFO) + self.server.start() + + # Send proper initialization parameters + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to pyright server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) + + # Verify that the server supports our required features + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] + + # Complete the initialization handshake + self.server.notify.initialized({}) + + # Wait for Pyright to complete its initial workspace analysis + # This prevents zombie processes by ensuring background tasks finish + self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO) + if self.analysis_complete.wait(timeout=5.0): + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + else: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() diff --git a/src/solidlsp/language_servers/rust_analyzer/initialize_params.json b/src/solidlsp/language_servers/rust_analyzer/initialize_params.json new file mode 100644 index 00000000..00368eed --- /dev/null +++ b/src/solidlsp/language_servers/rust_analyzer/initialize_params.json @@ -0,0 +1,917 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.82.0-insider" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": true, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span" + ] + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + }, + "experimental": { + "snippetTextEdit": true, + "codeActionGroup": true, + "hoverActions": true, + "serverStatusNotification": true, + "colorDiagnosticOutput": true, + "openServerLogs": true, + "localDocs": true, + "commands": { + "commands": [ + "rust-analyzer.runSingle", + "rust-analyzer.debugSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "editor.action.triggerParameterHints" + ] + } + } + }, + "initializationOptions": { + "cargoRunner": null, + "runnables": { + "extraEnv": null, + "problemMatcher": [ + "$rustc" + ], + "command": null, + "extraArgs": [] + }, + "statusBar": { + "clickAction": "openLogs" + }, + "server": { + "path": null, + "extraEnv": null + }, + "trace": { + "server": "verbose", + "extension": false + }, + "debug": { + "engine": "auto", + "sourceFileMap": { + "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" + }, + "openDebugPane": false, + "engineSettings": {} + }, + "restartServerOnConfigChange": false, + "typing": { + "continueCommentsOnNewline": true, + "autoClosingAngleBrackets": { + "enable": false + } + }, + "diagnostics": { + "previewRustcOutput": false, + "useRustcErrorCode": false, + "disabled": [], + "enable": true, + "experimental": { + "enable": false + }, + "remapPrefix": {}, + "warningsAsHint": [], + "warningsAsInfo": [] + }, + "discoverProjectRunner": null, + "showUnlinkedFileNotification": true, + "showDependenciesExplorer": true, + "assist": { + "emitMustUse": false, + "expressionFillDefault": "todo" + }, + "cachePriming": { + "enable": true, + "numThreads": 0 + }, + "cargo": { + "autoreload": true, + "buildScripts": { + "enable": true, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "overrideCommand": null, + "useRustcWrapper": true + }, + "cfgs": {}, + "extraArgs": [], + "extraEnv": {}, + "features": [], + "noDefaultFeatures": false, + "sysroot": "discover", + "sysrootSrc": null, + "target": null, + "unsetTest": [ + "core" + ] + }, + "checkOnSave": true, + "check": { + "allTargets": true, + "command": "check", + "extraArgs": [], + "extraEnv": {}, + "features": null, + "ignore": [], + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "noDefaultFeatures": null, + "overrideCommand": null, + "targets": null + }, + "completion": { + "autoimport": { + "enable": true + }, + "autoself": { + "enable": true + }, + "callable": { + "snippets": "fill_arguments" + }, + "fullFunctionSignatures": { + "enable": false + }, + "limit": null, + "postfix": { + "enable": true + }, + "privateEditable": { + "enable": false + }, + "snippets": { + "custom": { + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr" + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr" + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr" + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr" + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr" + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr" + } + } + } + }, + "files": { + "excludeDirs": [], + "watcher": "client" + }, + "highlightRelated": { + "breakPoints": { + "enable": true + }, + "closureCaptures": { + "enable": true + }, + "exitPoints": { + "enable": true + }, + "references": { + "enable": true + }, + "yieldPoints": { + "enable": true + } + }, + "hover": { + "actions": { + "debug": { + "enable": true + }, + "enable": true, + "gotoTypeDef": { + "enable": true + }, + "implementations": { + "enable": true + }, + "references": { + "enable": false + }, + "run": { + "enable": true + } + }, + "documentation": { + "enable": true, + "keywords": { + "enable": true + } + }, + "links": { + "enable": true + }, + "memoryLayout": { + "alignment": "hexadecimal", + "enable": true, + "niches": false, + "offset": "hexadecimal", + "size": "both" + } + }, + "imports": { + "granularity": { + "enforce": false, + "group": "crate" + }, + "group": { + "enable": true + }, + "merge": { + "glob": true + }, + "preferNoStd": false, + "preferPrelude": false, + "prefix": "plain" + }, + "inlayHints": { + "bindingModeHints": { + "enable": false + }, + "chainingHints": { + "enable": true + }, + "closingBraceHints": { + "enable": true, + "minLines": 25 + }, + "closureCaptureHints": { + "enable": false + }, + "closureReturnTypeHints": { + "enable": "never" + }, + "closureStyle": "impl_fn", + "discriminantHints": { + "enable": "never" + }, + "expressionAdjustmentHints": { + "enable": "never", + "hideOutsideUnsafe": false, + "mode": "prefix" + }, + "lifetimeElisionHints": { + "enable": "never", + "useParameterNames": false + }, + "maxLength": 25, + "parameterHints": { + "enable": true + }, + "reborrowHints": { + "enable": "never" + }, + "renderColons": true, + "typeHints": { + "enable": true, + "hideClosureInitialization": false, + "hideNamedConstructor": false + } + }, + "interpret": { + "tests": false + }, + "joinLines": { + "joinAssignments": true, + "joinElseIf": true, + "removeTrailingComma": true, + "unwrapTrivialBlock": true + }, + "lens": { + "debug": { + "enable": true + }, + "enable": true, + "forceCustomCommands": true, + "implementations": { + "enable": true + }, + "location": "above_name", + "references": { + "adt": { + "enable": false + }, + "enumVariant": { + "enable": false + }, + "method": { + "enable": false + }, + "trait": { + "enable": false + } + }, + "run": { + "enable": true + } + }, + "linkedProjects": [], + "lru": { + "capacity": null, + "query": { + "capacities": {} + } + }, + "notifications": { + "cargoTomlNotFound": true + }, + "numThreads": null, + "procMacro": { + "attributes": { + "enable": true + }, + "enable": true, + "ignored": {}, + "server": null + }, + "references": { + "excludeImports": false + }, + "rust": { + "analyzerTargetDir": null + }, + "rustc": { + "source": null + }, + "rustfmt": { + "extraArgs": [], + "overrideCommand": null, + "rangeFormatting": { + "enable": false + } + }, + "semanticHighlighting": { + "doc": { + "comment": { + "inject": { + "enable": true + } + } + }, + "nonStandardTokens": true, + "operator": { + "enable": true, + "specialization": { + "enable": false + } + }, + "punctuation": { + "enable": false, + "separate": { + "macro": { + "bang": false + } + }, + "specialization": { + "enable": false + } + }, + "strings": { + "enable": true + } + }, + "signatureInfo": { + "detail": "full", + "documentation": { + "enable": true + } + }, + "workspace": { + "symbol": { + "search": { + "kind": "only_types", + "limit": 128, + "scope": "workspace" + } + } + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json b/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json new file mode 100644 index 00000000..517d8b87 --- /dev/null +++ b/src/solidlsp/language_servers/rust_analyzer/runtime_dependencies.json @@ -0,0 +1,29 @@ +{ + "_description": "Used to download the runtime dependencies for running RustAnalyzer. Obtained from https://github.com/rust-lang/rust-analyzer/releases", + "runtimeDependencies": [ + { + "id": "RustAnalyzer", + "description": "RustAnalyzer for Linux (x64)", + "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-aarch64-apple-darwin.gz", + "platformId": "osx-arm64", + "archiveType": "gz", + "binaryName": "rust_analyzer" + }, + { + "id": "RustAnalyzer", + "description": "RustAnalyzer for Linux (x64)", + "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-unknown-linux-gnu.gz", + "platformId": "linux-x64", + "archiveType": "gz", + "binaryName": "rust_analyzer" + }, + { + "id": "RustAnalyzer", + "description": "RustAnalyzer for Windows (x64)", + "url": "https://github.com/rust-lang/rust-analyzer/releases/download/2023-10-09/rust-analyzer-x86_64-pc-windows-msvc.zip", + "platformId": "win-x64", + "archiveType": "zip", + "binaryName": "rust-analyzer.exe" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py new file mode 100644 index 00000000..eb072c49 --- /dev/null +++ b/src/solidlsp/language_servers/rust_analyzer/rust_analyzer.py @@ -0,0 +1,173 @@ +""" +Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. +""" + +import json +import logging +import os +import pathlib +import stat +import threading + +from overrides import override + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import FileUtils +from multilspy.multilspy_utils import PlatformUtils +from solidlsp.ls import SolidLanguageServer + + +class RustAnalyzer(SolidLanguageServer): + """ + Provides Rust specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Rust. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a RustAnalyzer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + rustanalyzer_executable_path = self.setup_runtime_dependencies(logger, config) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=rustanalyzer_executable_path, cwd=repository_root_path), + "rust", + ) + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["target"] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for rust_analyzer. + """ + platform_id = PlatformUtils.get_platform_id() + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + # assert platform_id.value in [ + # "linux-x64", + # "win-x64", + # ], "Only linux-x64 and win-x64 platform is supported for in multilspy at the moment" + + runtime_dependencies = d["runtimeDependencies"] + runtime_dependencies = [ + dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value + ] + assert len(runtime_dependencies) == 1 + dependency = runtime_dependencies[0] + + rustanalyzer_ls_dir = os.path.join(os.path.dirname(__file__), "static", "RustAnalyzer") + rustanalyzer_executable_path = os.path.join(rustanalyzer_ls_dir, dependency["binaryName"]) + if not os.path.exists(rustanalyzer_ls_dir): + os.makedirs(rustanalyzer_ls_dir) + if dependency["archiveType"] == "gz": + FileUtils.download_and_extract_archive( + logger, dependency["url"], rustanalyzer_executable_path, dependency["archiveType"] + ) + else: + FileUtils.download_and_extract_archive( + logger, dependency["url"], rustanalyzer_ls_dir, dependency["archiveType"] + ) + assert os.path.exists(rustanalyzer_executable_path) + os.chmod(rustanalyzer_executable_path, stat.S_IEXEC) + + return rustanalyzer_executable_path + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Rust Analyzer Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """ + Starts the Rust Analyzer Language Server + """ + + def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + self.resolve_main_method_available.set() + return + + def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def check_experimental_status(params): + if params["quiescent"] == True: + self.server_ready.set() + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting RustAnalyzer server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [":", ".", "'", "("], + "completionItem": {"labelDetailsSupport": True}, + } + self.server.notify.initialized({}) + self.completions_available.set() + + self.server_ready.wait() diff --git a/src/solidlsp/language_servers/solargraph/initialize_params.json b/src/solidlsp/language_servers/solargraph/initialize_params.json new file mode 100644 index 00000000..2cbb41f8 --- /dev/null +++ b/src/solidlsp/language_servers/solargraph/initialize_params.json @@ -0,0 +1,15 @@ +{ + "_description": "This file contains the initialization parameters for the Solargraph Language Server.", + "processId": "$processId", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} diff --git a/src/solidlsp/language_servers/solargraph/runtime_dependencies.json b/src/solidlsp/language_servers/solargraph/runtime_dependencies.json new file mode 100644 index 00000000..38cc2372 --- /dev/null +++ b/src/solidlsp/language_servers/solargraph/runtime_dependencies.json @@ -0,0 +1,11 @@ +{ + "_description": "This file contains URLs and other metadata required for downloading and installing the Solargraph language server.", + "runtimeDependencies": [ + { + "url": "https://rubygems.org/downloads/solargraph-0.51.1.gem", + "installCommand": "gem install solargraph -v 0.51.1", + "binaryName": "solargraph", + "archiveType": "gem" + } + ] +} diff --git a/src/solidlsp/language_servers/solargraph/solargraph.py b/src/solidlsp/language_servers/solargraph/solargraph.py new file mode 100644 index 00000000..d036ed1e --- /dev/null +++ b/src/solidlsp/language_servers/solargraph/solargraph.py @@ -0,0 +1,176 @@ +""" +Provides Ruby specific instantiation of the LanguageServer class using Solargraph. +Contains various configurations and settings specific to Ruby. +""" + +import json +import logging +import os +import pathlib +import stat +import subprocess +import threading +from typing import override + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from solidlsp.ls import SolidLanguageServer + + +class Solargraph(SolidLanguageServer): + """ + Provides Ruby specific instantiation of the LanguageServer class using Solargraph. + Contains various configurations and settings specific to Ruby. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a Solargraph instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + solargraph_executable_path = self.setup_runtime_dependencies(logger, config, repository_root_path) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=f"{solargraph_executable_path} stdio", cwd=repository_root_path), + "ruby", + ) + self.server_ready = threading.Event() + self.service_ready_event = threading.Event() + self.initialize_searcher_command_available = threading.Event() + self.resolve_main_method_available = threading.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in ["vendor"] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig, repository_root_path: str) -> str: + """ + Setup runtime dependencies for Solargraph. + """ + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), "r", encoding="utf-8") as f: + d = json.load(f) + del d["_description"] + + dependency = d["runtimeDependencies"][0] + + # Check if Ruby is installed + try: + result = subprocess.run(["ruby", "--version"], check=True, capture_output=True, cwd=repository_root_path) + ruby_version = result.stdout.strip() + logger.log(f"Ruby version: {ruby_version}", logging.INFO) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Error checking for Ruby installation: {e.stderr}") + except FileNotFoundError: + raise RuntimeError("Ruby is not installed. Please install Ruby before continuing.") + + # Check if solargraph is installed + try: + result = subprocess.run(["gem", "list", "^solargraph$", "-i"], check=False, capture_output=True, text=True, cwd=repository_root_path) + if result.stdout.strip() == "false": + logger.log("Installing Solargraph...", logging.INFO) + subprocess.run(dependency["installCommand"].split(), check=True, capture_output=True, cwd=repository_root_path) + + # Get the gem executable path directly + result = subprocess.run(["gem", "which", "solargraph"], check=True, capture_output=True, text=True, cwd=repository_root_path) + gem_path = result.stdout.strip() + bin_dir = os.path.join(os.path.dirname(os.path.dirname(gem_path)), "bin") + executable_path = os.path.join(bin_dir, "solargraph") + + if not os.path.exists(executable_path): + raise RuntimeError(f"Solargraph executable not found at {executable_path}") + + # Ensure the executable has the right permissions + os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IEXEC) + + return executable_path + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Failed to check or install Solargraph. {e.stderr}") + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the Solargraph Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), "r", encoding="utf-8") as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """ + Starts the Solargraph Language Server for Ruby + """ + + def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + self.resolve_main_method_available.set() + return + + def lang_status_handler(params): + # TODO: Should we wait for + # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}} + # Before proceeding? + if params["type"] == "ServiceReady" and params["message"] == "ServiceReady": + self.service_ready_event.set() + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("language/status", lang_status_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("language/actionableNotification", do_nothing) + + self.logger.log("Starting solargraph server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + self.logger.log(f"Sending init params: {json.dumps(initialize_params, indent=4)}", logging.INFO) + init_response = self.server.send.initialize(initialize_params) + self.logger.log(f"Received init response: {init_response}", logging.INFO) + assert init_response["capabilities"]["textDocumentSync"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "resolveProvider": True, + "triggerCharacters": [".", ":", "@"], + } + self.server.notify.initialized({}) + self.completions_available.set() + + self.server_ready.set() + self.server_ready.wait() diff --git a/src/solidlsp/language_servers/typescript_language_server/initialize_params.json b/src/solidlsp/language_servers/typescript_language_server/initialize_params.json new file mode 100644 index 00000000..6209ba47 --- /dev/null +++ b/src/solidlsp/language_servers/typescript_language_server/initialize_params.json @@ -0,0 +1,906 @@ +{ + "_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize", + "processId": "os.getpid()", + "clientInfo": { + "name": "Visual Studio Code - Insiders", + "version": "1.82.0-insider" + }, + "locale": "en", + "rootPath": "$rootPath", + "rootUri": "$rootUri", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": true, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": false + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": false + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0", + "allowedTags": [ + "ul", + "li", + "p", + "code", + "blockquote", + "ol", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "em", + "pre", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "div", + "del", + "a", + "strong", + "br", + "img", + "span" + ] + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + }, + "experimental": { + "snippetTextEdit": true, + "codeActionGroup": true, + "hoverActions": true, + "serverStatusNotification": true, + "colorDiagnosticOutput": true, + "openServerLogs": true, + "localDocs": true, + "commands": { + "commands": [ + "rust-analyzer.runSingle", + "rust-analyzer.debugSingle", + "rust-analyzer.showReferences", + "rust-analyzer.gotoLocation", + "editor.action.triggerParameterHints" + ] + } + } + }, + "initializationOptions": { + "cargoRunner": null, + "runnables": { + "extraEnv": null, + "problemMatcher": [ + "$rustc" + ], + "command": null, + "extraArgs": [] + }, + "statusBar": { + "clickAction": "openLogs" + }, + "server": { + "path": null, + "extraEnv": null + }, + "trace": { + "server": "verbose", + "extension": false + }, + "debug": { + "engine": "auto", + "sourceFileMap": { + "/rustc/": "${env:USERPROFILE}/.rustup/toolchains//lib/rustlib/src/rust" + }, + "openDebugPane": false, + "engineSettings": {} + }, + "restartServerOnConfigChange": false, + "typing": { + "continueCommentsOnNewline": true, + "autoClosingAngleBrackets": { + "enable": false + } + }, + "diagnostics": { + "previewRustcOutput": false, + "useRustcErrorCode": false, + "disabled": [], + "enable": true, + "experimental": { + "enable": false + }, + "remapPrefix": {}, + "warningsAsHint": [], + "warningsAsInfo": [] + }, + "discoverProjectRunner": null, + "showUnlinkedFileNotification": true, + "showDependenciesExplorer": true, + "assist": { + "emitMustUse": false, + "expressionFillDefault": "todo" + }, + "cachePriming": { + "enable": true, + "numThreads": 0 + }, + "cargo": { + "autoreload": true, + "buildScripts": { + "enable": true, + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "overrideCommand": null, + "useRustcWrapper": true + }, + "cfgs": {}, + "extraArgs": [], + "extraEnv": {}, + "features": [], + "noDefaultFeatures": false, + "sysroot": "discover", + "sysrootSrc": null, + "target": null, + "unsetTest": [ + "core" + ] + }, + "checkOnSave": true, + "check": { + "allTargets": true, + "command": "check", + "extraArgs": [], + "extraEnv": {}, + "features": null, + "ignore": [], + "invocationLocation": "workspace", + "invocationStrategy": "per_workspace", + "noDefaultFeatures": null, + "overrideCommand": null, + "targets": null + }, + "completion": { + "autoimport": { + "enable": true + }, + "autoself": { + "enable": true + }, + "callable": { + "snippets": "fill_arguments" + }, + "fullFunctionSignatures": { + "enable": false + }, + "limit": null, + "postfix": { + "enable": true + }, + "privateEditable": { + "enable": false + }, + "snippets": { + "custom": { + "Arc::new": { + "postfix": "arc", + "body": "Arc::new(${receiver})", + "requires": "std::sync::Arc", + "description": "Put the expression into an `Arc`", + "scope": "expr" + }, + "Rc::new": { + "postfix": "rc", + "body": "Rc::new(${receiver})", + "requires": "std::rc::Rc", + "description": "Put the expression into an `Rc`", + "scope": "expr" + }, + "Box::pin": { + "postfix": "pinbox", + "body": "Box::pin(${receiver})", + "requires": "std::boxed::Box", + "description": "Put the expression into a pinned `Box`", + "scope": "expr" + }, + "Ok": { + "postfix": "ok", + "body": "Ok(${receiver})", + "description": "Wrap the expression in a `Result::Ok`", + "scope": "expr" + }, + "Err": { + "postfix": "err", + "body": "Err(${receiver})", + "description": "Wrap the expression in a `Result::Err`", + "scope": "expr" + }, + "Some": { + "postfix": "some", + "body": "Some(${receiver})", + "description": "Wrap the expression in an `Option::Some`", + "scope": "expr" + } + } + } + }, + "files": { + "excludeDirs": [], + "watcher": "client" + }, + "highlightRelated": { + "breakPoints": { + "enable": true + }, + "closureCaptures": { + "enable": true + }, + "exitPoints": { + "enable": true + }, + "references": { + "enable": true + }, + "yieldPoints": { + "enable": true + } + }, + "hover": { + "actions": { + "debug": { + "enable": true + }, + "enable": true, + "gotoTypeDef": { + "enable": true + }, + "implementations": { + "enable": true + }, + "references": { + "enable": false + }, + "run": { + "enable": true + } + }, + "documentation": { + "enable": true, + "keywords": { + "enable": true + } + }, + "links": { + "enable": true + }, + "memoryLayout": { + "alignment": "hexadecimal", + "enable": true, + "niches": false, + "offset": "hexadecimal", + "size": "both" + } + }, + "imports": { + "granularity": { + "enforce": false, + "group": "crate" + }, + "group": { + "enable": true + }, + "merge": { + "glob": true + }, + "preferNoStd": false, + "preferPrelude": false, + "prefix": "plain" + }, + "inlayHints": { + "bindingModeHints": { + "enable": false + }, + "chainingHints": { + "enable": true + }, + "closingBraceHints": { + "enable": true, + "minLines": 25 + }, + "closureCaptureHints": { + "enable": false + }, + "closureReturnTypeHints": { + "enable": "never" + }, + "closureStyle": "impl_fn", + "discriminantHints": { + "enable": "never" + }, + "expressionAdjustmentHints": { + "enable": "never", + "hideOutsideUnsafe": false, + "mode": "prefix" + }, + "lifetimeElisionHints": { + "enable": "never", + "useParameterNames": false + }, + "maxLength": 25, + "parameterHints": { + "enable": true + }, + "reborrowHints": { + "enable": "never" + }, + "renderColons": true, + "typeHints": { + "enable": true, + "hideClosureInitialization": false, + "hideNamedConstructor": false + } + }, + "interpret": { + "tests": false + }, + "joinLines": { + "joinAssignments": true, + "joinElseIf": true, + "removeTrailingComma": true, + "unwrapTrivialBlock": true + }, + "lens": { + "debug": { + "enable": true + }, + "enable": true, + "forceCustomCommands": true, + "implementations": { + "enable": true + }, + "location": "above_name", + "references": { + "adt": { + "enable": false + }, + "enumVariant": { + "enable": false + }, + "method": { + "enable": false + }, + "trait": { + "enable": false + } + }, + "run": { + "enable": true + } + }, + "linkedProjects": [], + "lru": { + "capacity": null, + "query": { + "capacities": {} + } + }, + "notifications": { + "cargoTomlNotFound": true + }, + "numThreads": null, + "procMacro": { + "attributes": { + "enable": true + }, + "enable": true, + "ignored": {}, + "server": null + }, + "references": { + "excludeImports": false + }, + "rust": { + "analyzerTargetDir": null + }, + "rustc": { + "source": null + }, + "rustfmt": { + "extraArgs": [], + "overrideCommand": null, + "rangeFormatting": { + "enable": false + } + }, + "semanticHighlighting": { + "doc": { + "comment": { + "inject": { + "enable": true + } + } + }, + "nonStandardTokens": true, + "operator": { + "enable": true, + "specialization": { + "enable": false + } + }, + "punctuation": { + "enable": false, + "separate": { + "macro": { + "bang": false + } + }, + "specialization": { + "enable": false + } + }, + "strings": { + "enable": true + } + }, + "signatureInfo": { + "detail": "full", + "documentation": { + "enable": true + } + }, + "workspace": { + "symbol": { + "search": { + "kind": "only_types", + "limit": 128, + "scope": "workspace" + } + } + } + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": "$uri", + "name": "$name" + } + ] +} \ No newline at end of file diff --git a/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json b/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json new file mode 100644 index 00000000..90824a7c --- /dev/null +++ b/src/solidlsp/language_servers/typescript_language_server/runtime_dependencies.json @@ -0,0 +1,16 @@ +{ + "_description": "Used to download the runtime dependencies for running typescript-language-server. Obtained from https://github.com/typescript-language-server/typescript-language-server/releases", + "runtimeDependencies": [ + { + "id": "typescript", + "description": "typescript package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", + "command": "npm install --prefix ./ typescript@5.5.4" + }, + { + "id": "typescript-language-server", + "description": "typescript-language-server package for Linux, OSX, and Windows. Both x64 and arm64 are supported.", + "command": "npm install --prefix ./ typescript-language-server@4.3.3" + } + ] +} + diff --git a/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py new file mode 100644 index 00000000..e17a0987 --- /dev/null +++ b/src/solidlsp/language_servers/typescript_language_server/typescript_language_server.py @@ -0,0 +1,240 @@ +""" +Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. +""" + +import json +import logging +import os +import pathlib +import shutil +import subprocess +import threading +from time import sleep + +from overrides import override + +from multilspy.lsp_protocol_handler.lsp_types import InitializeParams +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.multilspy_config import MultilspyConfig +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import PlatformId, PlatformUtils +from solidlsp.ls import SolidLanguageServer + +# Platform-specific imports +if os.name != 'nt': # Unix-like systems + import pwd +else: + # Dummy pwd module for Windows + class pwd: + @staticmethod + def getpwuid(uid): + return type('obj', (), {'pw_name': os.environ.get('USERNAME', 'unknown')})() + + +# Conditionally import pwd module (Unix-only) +if not PlatformUtils.get_platform_id().value.startswith("win"): + import pwd + + +class TypeScriptLanguageServer(SolidLanguageServer): + """ + Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript. + """ + + def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): + """ + Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead. + """ + ts_lsp_executable_path = self.setup_runtime_dependencies(logger, config) + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path), + "typescript", + ) + self.server_ready = threading.Event() + self.initialize_searcher_command_available = threading.Event() + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + return super().is_ignored_dirname(dirname) or dirname in [ + "node_modules", + "dist", + "build", + "coverage", + ] + + def setup_runtime_dependencies(self, logger: MultilspyLogger, config: MultilspyConfig) -> str: + """ + Setup runtime dependencies for TypeScript Language Server. + """ + platform_id = PlatformUtils.get_platform_id() + + valid_platforms = [ + PlatformId.LINUX_x64, + PlatformId.LINUX_arm64, + PlatformId.OSX, + PlatformId.OSX_x64, + PlatformId.OSX_arm64, + PlatformId.WIN_x64, + PlatformId.WIN_arm64, + ] + assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment" + + with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json")) as f: + d = json.load(f) + del d["_description"] + + runtime_dependencies = d.get("runtimeDependencies", []) + tsserver_ls_dir = os.path.join(os.path.dirname(__file__), "static", "ts-lsp") + tsserver_executable_path = os.path.join(tsserver_ls_dir, "typescript-language-server") + + # Verify both node and npm are installed + is_node_installed = shutil.which('node') is not None + assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." + is_npm_installed = shutil.which('npm') is not None + assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + + # Install typescript and typescript-language-server if not already installed + if not os.path.exists(tsserver_ls_dir): + os.makedirs(tsserver_ls_dir, exist_ok=True) + for dependency in runtime_dependencies: + # Windows doesn't support the 'user' parameter and doesn't have pwd module + if PlatformUtils.get_platform_id().value.startswith("win"): + subprocess.run( + dependency["command"], + shell=True, + check=True, + cwd=tsserver_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + else: + # On Unix-like systems, run as non-root user + user = pwd.getpwuid(os.getuid()).pw_name + subprocess.run( + dependency["command"], + shell=True, + check=True, + user=user, + cwd=tsserver_ls_dir, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server") + + assert os.path.exists(tsserver_executable_path), "typescript-language-server executable not found. Please install typescript-language-server and try again." + return f"{tsserver_executable_path} --stdio" + + def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the TypeScript Language Server. + """ + with open(os.path.join(os.path.dirname(__file__), "initialize_params.json")) as f: + d = json.load(f) + + del d["_description"] + + d["processId"] = os.getpid() + assert d["rootPath"] == "$rootPath" + d["rootPath"] = repository_absolute_path + + assert d["rootUri"] == "$rootUri" + d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["uri"] == "$uri" + d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri() + + assert d["workspaceFolders"][0]["name"] == "$name" + d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path) + + return d + + def _start_server(self): + """ + Starts the TypeScript Language Server, waits for the server to be ready and yields the LanguageServer instance. + + Usage: + ``` + async with lsp.start_server(): + # LanguageServer has been initialized and ready to serve requests + await lsp.request_definition(...) + await lsp.request_references(...) + # Shutdown the LanguageServer on exit from scope + # LanguageServer has been shutdown + """ + + def register_capability_handler(params): + assert "registrations" in params + for registration in params["registrations"]: + if registration["method"] == "workspace/executeCommand": + self.initialize_searcher_command_available.set() + # TypeScript doesn't have a direct equivalent to resolve_main_method + # You might want to set a different flag or remove this line + # self.resolve_main_method_available.set() + return + + def execute_client_command_handler(params): + return [] + + def do_nothing(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + + def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.server_ready.set() + self.completions_available.set() + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) + + self.logger.log("Starting TypeScript server process", logging.INFO) + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to LSP server and awaiting response", + logging.INFO, + ) + init_response = self.server.send.initialize(initialize_params) + + # TypeScript-specific capability checks + assert init_response["capabilities"]["textDocumentSync"] == 2 + assert "completionProvider" in init_response["capabilities"] + assert init_response["capabilities"]["completionProvider"] == { + "triggerCharacters": ['.', '"', "'", '/', '@', '<'], + "resolveProvider": True + } + + self.server.notify.initialized({}) + if self.server_ready.wait(timeout=1.0): + self.logger.log("TypeScript server is ready", logging.INFO) + else: + self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) + # Fallback: assume server is ready after timeout + self.server_ready.set() + self.completions_available.set() + + @override + # For some reason, the LS may need longer to process this, so we just retry + def _send_references_request(self, relative_file_path: str, line: int, column: int): + # TODO: The LS doesn't return references contained in other files if it doesn't sleep. This is + # despite the LS having processed requests already. I don't know what causes this, but sleeping + # one second helps. It may be that sleeping only once is enough but that's hard to reliably test. + # It may be that even this 1sec is not enough in larger TS projects, at some point we should find what + # causes this and solve it. + sleep(1) + return super()._send_references_request(relative_file_path, line, column) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py new file mode 100644 index 00000000..9981ed1e --- /dev/null +++ b/src/solidlsp/ls.py @@ -0,0 +1,1635 @@ +import json +import logging +import os +import pathlib +import pickle +import re +import threading +from abc import abstractmethod, ABC +from collections import defaultdict +from contextlib import contextmanager +from copy import copy +from pathlib import Path, PurePath +from typing import Dict, Iterator, List, Optional, Tuple, Union, cast, Self + +import pathspec +import tqdm + +from multilspy import multilspy_types +from multilspy.language_server import LSPFileBuffer, GenericDocumentSymbol, ReferenceInSymbol +from multilspy.lsp_protocol_handler import lsp_types +from multilspy.lsp_protocol_handler import lsp_types as LSPTypes +from multilspy.lsp_protocol_handler.lsp_constants import LSPConstants +from multilspy.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink, SymbolKind +from multilspy.lsp_protocol_handler.server import ( + Error, + ProcessLaunchInfo, + StringDict, +) +from multilspy.multilspy_config import Language, MultilspyConfig +from multilspy.multilspy_exceptions import MultilspyException +from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_utils import FileUtils, PathUtils, TextUtils +from serena.text_utils import MatchedConsecutiveLines, search_files +from solidlsp.ls_handler import SolidLanguageServerHandler + + +class SolidLanguageServer(ABC): + """ + The LanguageServer class provides a language agnostic interface to the Language Server Protocol. + It is used to communicate with Language Servers of different programming languages. + """ + + # To be overridden and extended by subclasses + def is_ignored_dirname(self, dirname: str) -> bool: + """ + A language-specific condition for directories that should always be ignored. For example, venv + in Python and node_modules in JS/TS should be ignored always. + """ + return dirname.startswith('.') + + @classmethod + def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str, timeout: Optional[float] = None) -> "SolidLanguageServer": + """ + Creates a language specific LanguageServer instance based on the given configuration, and appropriate settings for the programming language. + + If language is Java, then ensure that jdk-17.0.6 or higher is installed, `java` is in PATH, and JAVA_HOME is set to the installation directory. + If language is JS/TS, then ensure that node (v18.16.0 or higher) is installed and in PATH. + + :param repository_root_path: The root path of the repository. + :param config: The Multilspy configuration. + :param logger: The logger to use. + :return LanguageServer: A language specific LanguageServer instance. + """ + # TODO: Apply timeout + if config.code_language == Language.PYTHON: + from solidlsp.language_servers.pyright_language_server.pyright_server import ( + PyrightServer, + ) + return PyrightServer(config, logger, repository_root_path) + # It used to be jedi, but pyright is a bit faster, and also more actively maintained + # Keeping the previous code for reference + # from multilspy.language_servers.jedi_language_server.jedi_server import ( + # JediServer, + # ) + + # return JediServer(config, logger, repository_root_path) + + elif config.code_language == Language.JAVA: + from solidlsp.language_servers.eclipse_jdtls.eclipse_jdtls import ( + EclipseJDTLS, + ) + return EclipseJDTLS(config, logger, repository_root_path) + + elif config.code_language == Language.KOTLIN: + from solidlsp.language_servers.kotlin_language_server.kotlin_language_server import ( + KotlinLanguageServer, + ) + return KotlinLanguageServer(config, logger, repository_root_path) + + elif config.code_language == Language.RUST: + from solidlsp.language_servers.rust_analyzer.rust_analyzer import ( + RustAnalyzer, + ) + return RustAnalyzer(config, logger, repository_root_path) + + elif config.code_language == Language.CSHARP: + from solidlsp.language_servers.omnisharp.omnisharp import OmniSharp + return OmniSharp(config, logger, repository_root_path) + + elif config.code_language in [Language.TYPESCRIPT, Language.JAVASCRIPT]: + from solidlsp.language_servers.typescript_language_server.typescript_language_server import ( + TypeScriptLanguageServer, + ) + return TypeScriptLanguageServer(config, logger, repository_root_path) + + elif config.code_language == Language.GO: + from solidlsp.language_servers.gopls.gopls import Gopls + return Gopls(config, logger, repository_root_path) + + elif config.code_language == Language.RUBY: + from solidlsp.language_servers.solargraph.solargraph import Solargraph + return Solargraph(config, logger, repository_root_path) + + elif config.code_language == Language.DART: + from solidlsp.language_servers.dart_language_server.dart_language_server import DartLanguageServer + return DartLanguageServer(config, logger, repository_root_path) + + elif config.code_language == Language.CPP: + from solidlsp.language_servers.clangd_language_server.clangd_language_server import ClangdLanguageServer + return ClangdLanguageServer(config, logger, repository_root_path) + + elif config.code_language == Language.PHP: + from solidlsp.language_servers.intelephense.intelephense import Intelephense + return Intelephense(config, logger, repository_root_path) + + else: + logger.log(f"Language {config.code_language} is not supported", logging.ERROR) + raise MultilspyException(f"Language {config.code_language} is not supported") + + def __init__( + self, + config: MultilspyConfig, + logger: MultilspyLogger, + repository_root_path: str, + process_launch_info: ProcessLaunchInfo, + language_id: str, + ): + """ + Initializes a LanguageServer instance. + + Do not instantiate this class directly. Use `LanguageServer.create` method instead. + + :param config: The Multilspy configuration. + :param logger: The logger to use. + :param repository_root_path: The root path of the repository. + :param process_launch_info: Each language server has a specific command used to start the server. + This parameter is the command to launch the language server process. + The command must pass appropriate flags to the binary, so that it runs in the stdio mode, + as opposed to HTTP, TCP modes supported by some language servers. + """ + self.logger = logger + self.repository_root_path: str = repository_root_path + self.logger.log(f"Creating language server instance for {repository_root_path=} with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) + + self.language_id = language_id + self.open_file_buffers: Dict[str, LSPFileBuffer] = {} + self.language = Language(language_id) + + # load cache first to prevent any racing conditions due to asyncio stuff + self._document_symbols_cache: dict[str, Tuple[str, Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]]] = {} + """Maps file paths to a tuple of (file_content_hash, result_of_request_document_symbols)""" + self._cache_lock = threading.Lock() + self._cache_has_changed: bool = False + self.load_cache() + + self.server_started = False + self.completions_available = threading.Event() + if config.trace_lsp_communication: + def logging_fn(source: str, target: str, msg: StringDict | str): + self.logger.log(f"LSP: {source} -> {target}: {str(msg)[:90]}...", self.logger.logger.level) + else: + logging_fn = None + + + # cmd is obtained from the child classes, which provide the language specific command to start the language server + # LanguageServerHandler provides the functionality to start the language server and communicate with it + self.logger.log(f"Creating language server instance with {language_id=} and process launch info: {process_launch_info}", logging.DEBUG) + self.server = SolidLanguageServerHandler( + process_launch_info, + logger=logging_fn, + start_independent_lsp_process=config.start_independent_lsp_process, + ) + + + # Set up the pathspec matcher for the ignored paths + # for all absolute paths in ignored_paths, convert them to relative paths + processed_patterns = [] + for pattern in set(config.ignored_paths): + # Normalize separators (pathspec expects forward slashes) + pattern = pattern.replace(os.path.sep, '/') + processed_patterns.append(pattern) + self.logger.log(f"Processing {len(processed_patterns)} ignored paths from the config", logging.DEBUG) + + # Create a pathspec matcher from the processed patterns + self._ignore_spec = pathspec.PathSpec.from_lines( + pathspec.patterns.GitWildMatchPattern, + processed_patterns + ) + + self._server_context = None + + def get_ignore_spec(self) -> pathspec.PathSpec: + """Returns the pathspec matcher for the paths that were configured to be ignored through + the multilspy config. + + This is is a subset of the full language-specific ignore spec that determines + which files are relevant for the language server. + + This matcher is useful for operations outside of the language server, + such as when searching for relevant non-language files in the project. + """ + return self._ignore_spec + + def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool: + """ + Determine if a path should be ignored based on file type + and ignore patterns. + + :param relative_path: Relative path to check + :param ignore_unsupported_files: whether files that are not supported source files should be ignored + + :return: True if the path should be ignored, False otherwise + """ + abs_path = os.path.join(self.repository_root_path, relative_path) + if not os.path.exists(abs_path): + raise FileNotFoundError(f"File {abs_path} not found, the ignore check cannot be performed") + + # Check file extension if it's a file + is_file = os.path.isfile(abs_path) + if is_file and ignore_unsupported_files: + fn_matcher = self.language.get_source_fn_matcher() + if not fn_matcher.is_relevant_filename(abs_path): + return True + + # Create normalized path for consistent handling + rel_path = Path(relative_path) + + # Check each part of the path against always fulfilled ignore conditions + dir_parts = rel_path.parts + if is_file: + dir_parts = dir_parts[:-1] + for part in dir_parts: + if not part: # Skip empty parts (e.g., from leading '/') + continue + if self.is_ignored_dirname(part): + return True + + # Use pathspec for gitignore-style pattern matching + # Normalize path separators for pathspec (it expects forward slashes) + normalized_path = str(rel_path).replace(os.path.sep, '/') + + # pathspec can't handle the matching of directories if they don't end with a slash! + # see https://github.com/cpburnz/python-pathspec/issues/89 + if os.path.isdir(os.path.join(self.repository_root_path, normalized_path)) and not normalized_path.endswith('/'): + normalized_path = normalized_path + '/' + + # Use the pathspec matcher to check if the path matches any ignore pattern + if self.get_ignore_spec().match_file(normalized_path): + return True + + return False + + def _shutdown(self, timeout: float = 5.0): + """ + A robust shutdown process designed to terminate cleanly on all platforms, including Windows, + by explicitly closing all I/O pipes. + """ + if not self.server.is_running(): + self.logger.log("Server process not running, skipping shutdown.", logging.DEBUG) + return + + self.logger.log(f"Initiating final robust shutdown with a {timeout}s timeout...", logging.INFO) + process = self.server.process + reader_tasks = list(self.server.tasks.values()) + + # --- Main Shutdown Logic --- + # Stage 1: Graceful Termination Request + # Send LSP shutdown and close stdin to signal no more input. + try: + self.server.shutdown() + if process.stdin and not process.stdin.is_closing(): + process.stdin.close() + except Exception: + pass # Ignore errors here, we are proceeding to terminate anyway. + + # Stage 2: Terminate and Concurrently Drain stdout/stderr + process.terminate() + + """ + except asyncio.TimeoutError: + # Stage 3: Forceful Kill + self.logger.log("Graceful termination failed. Forcefully killing process...", logging.WARNING) + if self.server.is_running(): + try: + process.kill() + # Wait for the killed process to be reaped by the OS. + await process.wait() + except Exception as e: + self.logger.log(f"Error during forceful kill: {e}", logging.ERROR) + except Exception as e: + self.logger.log(f"An unexpected error occurred during shutdown logic: {e}", logging.ERROR) + + finally: + # === STAGE 4: EXPLICIT TASK & PIPE CLEANUP === + self.logger.log("Performing final task cancellation and pipe handle cleanup...", logging.DEBUG) + + # 1. Cancel any lingering reader tasks. + for task in reader_tasks: + if not task.done(): + task.cancel() + # Wait for cancellations to complete. + await asyncio.gather(*reader_tasks, return_exceptions=True) + self.server.tasks = {} + + # 2. Explicitly close each pipe to release OS handles. + for pipe in [process.stdin, process.stdout, process.stderr]: + if pipe and hasattr(pipe, 'is_closing') and not pipe.is_closing(): + try: + pipe.close() + except Exception: + pass + + # 3. Null out the process object in the handler. + self.server.process = None + self.logger.log("Shutdown sequence fully finished.", logging.DEBUG) + """ + + @contextmanager + def start_server(self) -> Iterator["SolidLanguageServer"]: + self.start() + yield self + self.stop() + + def _start_server_process(self) -> None: + self.server_started = True + self._start_server() + + @abstractmethod + def _start_server(self): + pass + + @contextmanager + def open_file(self, relative_file_path: str) -> Iterator[LSPFileBuffer]: + """ + Open a file in the Language Server. This is required before making any requests to the Language Server. + + :param relative_file_path: The relative path of the file to open. + """ + if not self.server_started: + self.logger.log( + "open_file called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + + if uri in self.open_file_buffers: + assert self.open_file_buffers[uri].uri == uri + assert self.open_file_buffers[uri].ref_count >= 1 + + self.open_file_buffers[uri].ref_count += 1 + yield self.open_file_buffers[uri] + self.open_file_buffers[uri].ref_count -= 1 + else: + contents = FileUtils.read_file(self.logger, absolute_file_path) + + version = 0 + self.open_file_buffers[uri] = LSPFileBuffer(uri, contents, version, self.language_id, 1) + + self.server.notify.did_open_text_document( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: uri, + LSPConstants.LANGUAGE_ID: self.language_id, + LSPConstants.VERSION: 0, + LSPConstants.TEXT: contents, + } + } + ) + yield self.open_file_buffers[uri] + self.open_file_buffers[uri].ref_count -= 1 + + if self.open_file_buffers[uri].ref_count == 0: + self.server.notify.did_close_text_document( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: uri, + } + } + ) + del self.open_file_buffers[uri] + + def insert_text_at_position( + self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str + ) -> multilspy_types.Position: + """ + Insert text at the given line and column in the given file and return + the updated cursor position after inserting the text. + + :param relative_file_path: The relative path of the file to open. + :param line: The line number at which text should be inserted. + :param column: The column number at which text should be inserted. + :param text_to_be_inserted: The text to insert. + """ + if not self.server_started: + self.logger.log( + "insert_text_at_position called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + + # Ensure the file is open + assert uri in self.open_file_buffers + + file_buffer = self.open_file_buffers[uri] + file_buffer.version += 1 + + new_contents, new_l, new_c = TextUtils.insert_text_at_position(file_buffer.contents, line, column, text_to_be_inserted) + file_buffer.contents = new_contents + self.server.notify.did_change_text_document( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.VERSION: file_buffer.version, + LSPConstants.URI: file_buffer.uri, + }, + LSPConstants.CONTENT_CHANGES: [ + { + LSPConstants.RANGE: { + "start": {"line": line, "character": column}, + "end": {"line": line, "character": column}, + }, + "text": text_to_be_inserted, + } + ], + } + ) + return multilspy_types.Position(line=new_l, character=new_c) + + def delete_text_between_positions( + self, + relative_file_path: str, + start: multilspy_types.Position, + end: multilspy_types.Position, + ) -> str: + """ + Delete text between the given start and end positions in the given file and return the deleted text. + """ + if not self.server_started: + self.logger.log( + "insert_text_at_position called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + + # Ensure the file is open + assert uri in self.open_file_buffers + + file_buffer = self.open_file_buffers[uri] + file_buffer.version += 1 + new_contents, deleted_text = TextUtils.delete_text_between_positions(file_buffer.contents, start_line=start["line"], start_col=start["character"], end_line=end["line"], end_col=end["character"]) + file_buffer.contents = new_contents + self.server.notify.did_change_text_document( + { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.VERSION: file_buffer.version, + LSPConstants.URI: file_buffer.uri, + }, + LSPConstants.CONTENT_CHANGES: [{LSPConstants.RANGE: {"start": start, "end": end}, "text": ""}], + } + ) + return deleted_text + + def _send_definition_request(self, definition_params: DefinitionParams) -> Union[Definition, List[LocationLink], None]: + return self.server.send.definition(definition_params) + + def request_definition( + self, relative_file_path: str, line: int, column: int + ) -> List[multilspy_types.Location]: + """ + Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server + for the symbol at the given line and column in the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up + :param line: The line number of the symbol + :param column: The column number of the symbol + + :return List[multilspy_types.Location]: A list of locations where the symbol is defined + """ + + if not self.server_started: + self.logger.log( + "find_function_definition called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + with self.open_file(relative_file_path): + # sending request to the language server and waiting for response + definition_params = cast(DefinitionParams, { + LSPConstants.TEXT_DOCUMENT: { + LSPConstants.URI: pathlib.Path( + str(PurePath(self.repository_root_path, relative_file_path)) + ).as_uri() + }, + LSPConstants.POSITION: { + LSPConstants.LINE: line, + LSPConstants.CHARACTER: column, + }, + }) + response = self._send_definition_request(definition_params) + + ret: List[multilspy_types.Location] = [] + if isinstance(response, list): + # response is either of type Location[] or LocationLink[] + for item in response: + assert isinstance(item, dict) + if LSPConstants.URI in item and LSPConstants.RANGE in item: + new_item: multilspy_types.Location = {} + new_item.update(item) + new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) + new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) + ret.append(multilspy_types.Location(new_item)) + elif ( + LSPConstants.ORIGIN_SELECTION_RANGE in item + and LSPConstants.TARGET_URI in item + and LSPConstants.TARGET_RANGE in item + and LSPConstants.TARGET_SELECTION_RANGE in item + ): + new_item: multilspy_types.Location = {} + new_item["uri"] = item[LSPConstants.TARGET_URI] + new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) + new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) + new_item["range"] = item[LSPConstants.TARGET_SELECTION_RANGE] + ret.append(multilspy_types.Location(**new_item)) + else: + assert False, f"Unexpected response from Language Server: {item}" + elif isinstance(response, dict): + # response is of type Location + assert LSPConstants.URI in response + assert LSPConstants.RANGE in response + + new_item: multilspy_types.Location = {} + new_item.update(response) + new_item["absolutePath"] = PathUtils.uri_to_path(new_item["uri"]) + new_item["relativePath"] = PathUtils.get_relative_path(new_item["absolutePath"], self.repository_root_path) + ret.append(multilspy_types.Location(**new_item)) + elif response is None: + # Some language servers return None when they cannot find a definition + # This is expected for certain symbol types like generics or types with incomplete information + self.logger.log( + f"Language server returned None for definition request at {relative_file_path}:{line}:{column}", + logging.WARNING, + ) + else: + assert False, f"Unexpected response from Language Server: {response}" + + return ret + + # Some LS cause problems with this, so the call is isolated from the rest to allow overriding in subclasses + def _send_references_request(self, relative_file_path: str, line: int, column: int) -> List[lsp_types.Location] | None: + return self.server.send.references( + { + "textDocument": {"uri": PathUtils.path_to_uri(os.path.join(self.repository_root_path, relative_file_path))}, + "position": {"line": line, "character": column}, + "context": {"includeDeclaration": False}, + } + ) + + def request_references( + self, relative_file_path: str, line: int, column: int + ) -> List[multilspy_types.Location]: + """ + Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server + to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. + Filters out references located in ignored directories. + + :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up + :param line: The line number of the symbol + :param column: The column number of the symbol + + :return: A list of locations where the symbol is referenced (excluding ignored directories) + """ + + if not self.server_started: + self.logger.log( + "request_references called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("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: + 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. " + ) from e + raise + if response is None: + return [] + + ret: List[multilspy_types.Location] = [] + assert isinstance(response, list), f"Unexpected response from Language Server (expected list, got {type(response)}): {response}" + for item in response: + assert isinstance(item, dict), f"Unexpected response from Language Server (expected dict, got {type(item)}): {item}" + assert LSPConstants.URI in item + assert LSPConstants.RANGE in item + + abs_path = PathUtils.uri_to_path(item[LSPConstants.URI]) + rel_path = Path(abs_path).relative_to(self.repository_root_path) + if self.is_ignored_path(str(rel_path)): + self.logger.log(f"Ignoring reference in {rel_path} since it should be ignored", logging.DEBUG) + continue + + new_item: multilspy_types.Location = {} + new_item.update(item) + new_item["absolutePath"] = str(abs_path) + new_item["relativePath"] = str(rel_path) + ret.append(multilspy_types.Location(**new_item)) + + return ret + + def request_references_with_content( + self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0 + ) -> List[MatchedConsecutiveLines]: + """ + Like request_references, but returns the content of the lines containing the references, not just the locations. + + :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up + :param line: The line number of the symbol + :param column: The column number of the symbol + :param context_lines_before: The number of lines to include in the context before the line containing the reference + :param context_lines_after: The number of lines to include in the context after the line containing the reference + + :return: A list of MatchedConsecutiveLines objects, one for each reference. + """ + references = self.request_references(relative_file_path, line, column) + return [self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) for ref in references] + + def retrieve_full_file_content(self, relative_file_path: str) -> str: + """ + Retrieve the full content of the given file. + """ + with self.open_file(relative_file_path) as file_data: + return file_data.contents + + def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines: + """ + Retrieve the content of the given file around the given line. + + :param relative_file_path: The relative path of the file to retrieve the content from + :param line: The line number to retrieve the content around + :param context_lines_before: The number of lines to retrieve before the given line + :param context_lines_after: The number of lines to retrieve after the given line + + :return MatchedConsecutiveLines: A container with the desired lines. + """ + with self.open_file(relative_file_path) as file_data: + file_contents = file_data.contents + return MatchedConsecutiveLines.from_file_contents(file_contents, line=line, context_lines_before=context_lines_before, context_lines_after=context_lines_after, source_file_path=relative_file_path) + + def request_completions( + self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False + ) -> List[multilspy_types.CompletionItem]: + """ + Raise a [textDocument/completion](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion) request to the Language Server + to find completions at the given line and column in the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file that has the symbol for which completions should be looked up + :param line: The line number of the symbol + :param column: The column number of the symbol + + :return List[multilspy_types.CompletionItem]: A list of completions + """ + with self.open_file(relative_file_path): + open_file_buffer = self.open_file_buffers[ + pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() + ] + completion_params: LSPTypes.CompletionParams = { + "position": {"line": line, "character": column}, + "textDocument": {"uri": open_file_buffer.uri}, + "context": {"triggerKind": LSPTypes.CompletionTriggerKind.Invoked}, + } + response: Union[List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None] = None + + num_retries = 0 + while response is None or (response["isIncomplete"] and num_retries < 30): + self.completions_available.wait() + response: Union[ + List[LSPTypes.CompletionItem], LSPTypes.CompletionList, None + ] = self.server.send.completion(completion_params) + if isinstance(response, list): + response = {"items": response, "isIncomplete": False} + num_retries += 1 + + # TODO: Understand how to appropriately handle `isIncomplete` + if response is None or (response["isIncomplete"] and not(allow_incomplete)): + return [] + + if "items" in response: + response = response["items"] + + response: List[LSPTypes.CompletionItem] = response + + # TODO: Handle the case when the completion is a keyword + items = [item for item in response if item["kind"] != LSPTypes.CompletionItemKind.Keyword] + + completions_list: List[multilspy_types.CompletionItem] = [] + + for item in items: + assert "insertText" in item or "textEdit" in item + assert "kind" in item + completion_item = {} + if "detail" in item: + completion_item["detail"] = item["detail"] + + if "label" in item: + completion_item["completionText"] = item["label"] + completion_item["kind"] = item["kind"] + elif "insertText" in item: + completion_item["completionText"] = item["insertText"] + completion_item["kind"] = item["kind"] + elif "textEdit" in item and "newText" in item["textEdit"]: + completion_item["completionText"] = item["textEdit"]["newText"] + completion_item["kind"] = item["kind"] + elif "textEdit" in item and "range" in item["textEdit"]: + new_dot_lineno, new_dot_colno = ( + completion_params["position"]["line"], + completion_params["position"]["character"], + ) + assert all( + ( + item["textEdit"]["range"]["start"]["line"] == new_dot_lineno, + item["textEdit"]["range"]["start"]["character"] == new_dot_colno, + item["textEdit"]["range"]["start"]["line"] == item["textEdit"]["range"]["end"]["line"], + item["textEdit"]["range"]["start"]["character"] + == item["textEdit"]["range"]["end"]["character"], + ) + ) + + completion_item["completionText"] = item["textEdit"]["newText"] + completion_item["kind"] = item["kind"] + elif "textEdit" in item and "insert" in item["textEdit"]: + assert False + else: + assert False + + completion_item = multilspy_types.CompletionItem(**completion_item) + completions_list.append(completion_item) + + return [ + json.loads(json_repr) + for json_repr in set([json.dumps(item, sort_keys=True) for item in completions_list]) + ] + + def request_document_symbols(self, relative_file_path: str, include_body: bool = False) -> Tuple[List[multilspy_types.UnifiedSymbolInformation], List[multilspy_types.UnifiedSymbolInformation]]: + """ + Raise a [textDocument/documentSymbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol) request to the Language Server + to find symbols in the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file that has the symbols + :param include_body: whether to include the body of the symbols in the result. + :return: A list of symbols in the file, and a list of root symbols that represent the tree structure of the symbols. + All symbols will have a location, a children, and a parent attribute, + where the parent attribute is None for root symbols. + Note that this is slightly different from the call to request_full_symbol_tree, + where the parent attribute will be the file symbol which in turn may have a package symbol as parent. + If you need a symbol tree that contains file symbols as well, you should use `request_full_symbol_tree` instead. + """ + # TODO: it's kinda dumb to not use the cache if include_body is False after include_body was True once + # Should be fixed in the future, it's a small performance optimization + cache_key = f"{relative_file_path}-{include_body}" + with self.open_file(relative_file_path) as file_data: + with self._cache_lock: + file_hash_and_result = self._document_symbols_cache.get(cache_key) + if file_hash_and_result is not None: + file_hash, result = file_hash_and_result + if file_hash == file_data.content_hash: + self.logger.log(f"Returning cached document symbols for {relative_file_path}", logging.DEBUG) + return result + else: + self.logger.log(f"Content for {relative_file_path} has changed. Will overwrite in-memory cache", logging.DEBUG) + else: + self.logger.log(f"No cache hit for symbols with {include_body=} in {relative_file_path}", logging.DEBUG) + + self.logger.log(f"Requesting document symbols for {relative_file_path} from the Language Server", logging.DEBUG) + response = self.server.send.document_symbol( + { + "textDocument": { + "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() + } + } + ) + self.logger.log(f"Received {len(response) if response is not None else None} document symbols for {relative_file_path} from the Language Server", logging.DEBUG) + + def turn_item_into_symbol_with_children(item: GenericDocumentSymbol): + item = cast(multilspy_types.UnifiedSymbolInformation, item) + absolute_path = os.path.join(self.repository_root_path, relative_file_path) + + # handle missing entries in location + if "location" not in item: + uri = pathlib.Path(absolute_path).as_uri() + assert "range" in item + tree_location = multilspy_types.Location( + uri=uri, + range=item['range'], + absolutePath=absolute_path, + relativePath=relative_file_path, + ) + item['location'] = tree_location + location = item["location"] + if "absolutePath" not in location: + location["absolutePath"] = absolute_path + if "relativePath" not in location: + location["relativePath"] = relative_file_path + if include_body: + item['body'] = self.retrieve_symbol_body(item) + # handle missing selectionRange + if "selectionRange" not in item: + if "range" in item: + item["selectionRange"] = item["range"] + else: + item["selectionRange"] = item["location"]["range"] + children = item.get(LSPConstants.CHILDREN, []) + for child in children: + child["parent"] = item + item[LSPConstants.CHILDREN] = children + + flat_all_symbol_list: List[multilspy_types.UnifiedSymbolInformation] = [] + assert isinstance(response, list), f"Unexpected response from Language Server: {response}" + root_nodes: List[multilspy_types.UnifiedSymbolInformation] = [] + for root_item in response: + if "range" not in root_item and "location" not in root_item: + if root_item["kind"] in [SymbolKind.File, SymbolKind.Module]: + ... + + # mutation is more convenient than creating a new dict, + # so we cast and rename the var after the mutating call to turn_item_into_symbol_with_children + # which turned and item into a "symbol" + turn_item_into_symbol_with_children(root_item) + root_symbol = cast(multilspy_types.UnifiedSymbolInformation, root_item) + root_symbol["parent"] = None + + root_nodes.append(root_symbol) + assert isinstance(root_symbol, dict) + assert LSPConstants.NAME in root_symbol + assert LSPConstants.KIND in root_symbol + + if LSPConstants.CHILDREN in root_symbol: + # TODO: l_tree should be a list of TreeRepr. Define the following function to return TreeRepr as well + + def visit_tree_nodes_and_build_tree_repr(node: GenericDocumentSymbol) -> List[multilspy_types.UnifiedSymbolInformation]: + node = cast(multilspy_types.UnifiedSymbolInformation, node) + l: List[multilspy_types.UnifiedSymbolInformation] = [] + turn_item_into_symbol_with_children(node) + assert LSPConstants.CHILDREN in node + children = node[LSPConstants.CHILDREN] + l.append(node) + for child in children: + l.extend(visit_tree_nodes_and_build_tree_repr(child)) + return l + + flat_all_symbol_list.extend(visit_tree_nodes_and_build_tree_repr(root_symbol)) + else: + flat_all_symbol_list.append(multilspy_types.UnifiedSymbolInformation(**root_symbol)) + + result = flat_all_symbol_list, root_nodes + self.logger.log(f"Caching document symbols for {relative_file_path}", logging.DEBUG) + with self._cache_lock: + self._document_symbols_cache[cache_key] = (file_data.content_hash, result) + self._cache_has_changed = True + return result + + def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: + """ + Will go through all files in the project or within a relative path and build a tree of symbols. + Note: this may be slow the first time it is called, especially if `within_relative_path` is not used to restrict the search. + + For each file, a symbol of kind File (2) will be created. For directories, a symbol of kind Package (4) will be created. + All symbols will have a children attribute, thereby representing the tree structure of all symbols in the project + that are within the repository. + All symbols except the root packages will have a parent attribute. + Will ignore directories starting with '.', language-specific defaults + and user-configured directories (e.g. from .gitignore). + + :param within_relative_path: pass a relative path to only consider symbols within this path. + If a file is passed, only the symbols within this file will be considered. + If a directory is passed, all files within this directory will be considered. + :param include_body: whether to include the body of the symbols in the result. + + :return: A list of root symbols representing the top-level packages/modules in the project. + """ + + if within_relative_path is not None: + within_abs_path = os.path.join(self.repository_root_path, within_relative_path) + if not os.path.exists(within_abs_path): + raise FileNotFoundError(f"File or directory not found: {within_abs_path}") + if os.path.isfile(within_abs_path): + if self.is_ignored_path(within_relative_path): + self.logger.log(f"You passed a file explicitly, but it is ignored. This is probably an error. File: {within_relative_path}", logging.ERROR) + return [] + else: + _, root_nodes = self.request_document_symbols(within_relative_path, include_body=include_body) + return root_nodes + + # Helper function to recursively process directories + def process_directory(rel_dir_path: str) -> List[multilspy_types.UnifiedSymbolInformation]: + abs_dir_path = self.repository_root_path if rel_dir_path == "." else os.path.join(self.repository_root_path, rel_dir_path) + abs_dir_path = os.path.realpath(abs_dir_path) + + if self.is_ignored_path(str(Path(abs_dir_path).relative_to(self.repository_root_path))): + self.logger.log(f"Skipping directory: {rel_dir_path}\n(because it should be ignored)", logging.DEBUG) + return [] + + result = [] + try: + contained_dir_or_file_names = os.listdir(abs_dir_path) + except OSError: + return [] + + # Create package symbol for directory + package_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + name=os.path.basename(abs_dir_path), + kind=multilspy_types.SymbolKind.Package, + location=multilspy_types.Location( + uri=str(pathlib.Path(abs_dir_path).as_uri()), + range={"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 0}}, + absolutePath=str(abs_dir_path), + relativePath=str(Path(abs_dir_path).resolve().relative_to(self.repository_root_path)), + ), + children=[] + ) + result.append(package_symbol) + + for contained_dir_or_file_name in contained_dir_or_file_names: + contained_dir_or_file_abs_path = os.path.join(abs_dir_path, contained_dir_or_file_name) + contained_dir_or_file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) + if self.is_ignored_path(contained_dir_or_file_rel_path): + self.logger.log(f"Skipping item: {contained_dir_or_file_rel_path}\n(because it should be ignored)", logging.DEBUG) + continue + + if os.path.isdir(contained_dir_or_file_abs_path): + child_symbols = process_directory(contained_dir_or_file_rel_path) + package_symbol["children"].extend(child_symbols) + for child in child_symbols: + child["parent"] = package_symbol + + elif os.path.isfile(contained_dir_or_file_abs_path): + _, file_root_nodes = self.request_document_symbols(contained_dir_or_file_rel_path, include_body=include_body) + + # Create file symbol, link with children + file_rel_path = str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)) + with self.open_file(file_rel_path) as file_data: + fileRange = self._get_range_from_file_content(file_data.contents) + file_symbol = multilspy_types.UnifiedSymbolInformation( # type: ignore + name=os.path.splitext(contained_dir_or_file_name)[0], + kind=multilspy_types.SymbolKind.File, + range=fileRange, + selectionRange=fileRange, + location=multilspy_types.Location( + uri=str(pathlib.Path(contained_dir_or_file_abs_path).as_uri()), + range=fileRange, + absolutePath=str(contained_dir_or_file_abs_path), + relativePath=str(Path(contained_dir_or_file_abs_path).resolve().relative_to(self.repository_root_path)), + ), + children=file_root_nodes, + parent=package_symbol, + ) + for child in file_root_nodes: + child["parent"] = file_symbol + + # Link file symbol with package + package_symbol["children"].append(file_symbol) + + # TODO: Not sure if this is actually still needed given recent changes to relative path handling + def fix_relative_path(nodes: List[multilspy_types.UnifiedSymbolInformation]): + for node in nodes: + if "location" in node and "relativePath" in node["location"]: + path = Path(node["location"]["relativePath"]) + if path.is_absolute(): + try: + path = path.relative_to(self.repository_root_path) + node["location"]["relativePath"] = str(path) + except Exception: + pass + if "children" in node: + fix_relative_path(node["children"]) + + fix_relative_path(file_root_nodes) + + return result + + # Start from the root or the specified directory + start_rel_path = within_relative_path or "." + return process_directory(start_rel_path) + + @staticmethod + def _get_range_from_file_content(file_content: str) -> multilspy_types.Range: + """ + Get the range for the given file. + """ + lines = file_content.split("\n") + end_line = len(lines) + end_column = len(lines[-1]) + return multilspy_types.Range( + start=multilspy_types.Position(line=0, character=0), + end=multilspy_types.Position(line=end_line, character=end_column) + ) + + def request_dir_overview(self, relative_dir_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + """ + An overview of the given directory. + + Maps relative paths of all contained files to info about top-level symbols in the file + (name, kind, line, column). + """ + symbol_tree = self.request_full_symbol_tree(relative_dir_path) + # Initialize result dictionary + result: dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]] = defaultdict(list) + + # Helper function to process a symbol and its children + def process_symbol(symbol: multilspy_types.UnifiedSymbolInformation): + if symbol["kind"] == multilspy_types.SymbolKind.File: + # For file symbols, process their children (top-level symbols) + for child in symbol["children"]: + assert "location" in child + assert "selectionRange" in child + path = Path(child["location"]["absolutePath"]).resolve().relative_to(self.repository_root_path) + result[str(path)].append(( + child["name"], + child["kind"], + child["selectionRange"]["start"]["line"], + child["selectionRange"]["start"]["character"] + )) + # For package/directory symbols, process their children + for child in symbol["children"]: + process_symbol(child) + + # Process each root symbol + for root in symbol_tree: + process_symbol(root) + return result + + def request_document_overview(self, relative_file_path: str) -> list[tuple[str, multilspy_types.SymbolKind, int, int]]: + """ + An overview of the given file. + Returns the list of tuples (name, kind, line, column) of all top-level symbols in the file. + """ + _, document_roots = self.request_document_symbols(relative_file_path) + result = [] + for root in document_roots: + try: + result.append( + ( root["name"], + root["kind"], + root["selectionRange"]["start"]["line"], + root["selectionRange"]["start"]["character"],) + ) + except KeyError as e: + raise KeyError( + f"Could not process symbol of name {root.get('name', 'unknown')} in {relative_file_path=}" + ) from e + return result + + def request_overview(self, within_relative_path: str) -> dict[str, list[tuple[str, multilspy_types.SymbolKind, int, int]]]: + """ + An overview of all symbols in the given file or directory. + + :param within_relative_path: the relative path to the file or directory to get the overview of. + :return: A mapping of all relative paths analyzed to lists of tuples (name, kind, line, column) of all top-level symbols in the corresponding file. + """ + abs_path = (Path(self.repository_root_path) / within_relative_path).resolve() + if not abs_path.exists(): + raise FileNotFoundError(f"File or directory not found: {abs_path}") + + if abs_path.is_file(): + symbols_overview = self.request_document_overview(within_relative_path) + return {within_relative_path: symbols_overview} + else: + return self.request_dir_overview(within_relative_path) + + def request_hover(self, relative_file_path: str, line: int, column: int) -> Union[multilspy_types.Hover, None]: + """ + Raise a [textDocument/hover](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover) request to the Language Server + to find the hover information at the given line and column in the given file. Wait for the response and return the result. + + :param relative_file_path: The relative path of the file that has the hover information + :param line: The line number of the symbol + :param column: The column number of the symbol + + :return None + """ + with self.open_file(relative_file_path): + response = self.server.send.hover( + { + "textDocument": { + "uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri() + }, + "position": { + "line": line, + "character": column, + }, + } + ) + + if response is None: + return None + + assert isinstance(response, dict) + + return multilspy_types.Hover(**response) + + # ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA -------------------- + + def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation | LSPTypes.DocumentSymbol | LSPTypes.SymbolInformation) -> str: + """ + Load the body of the given symbol. If the body is already contained in the symbol, just return it. + """ + existing_body = symbol.get("body", None) + if existing_body: + return existing_body + + assert "location" in symbol + symbol_start_line = symbol["location"]["range"]["start"]["line"] + symbol_end_line = symbol["location"]["range"]["end"]["line"] + assert "relativePath" in symbol["location"] + symbol_file = self.retrieve_full_file_content(symbol["location"]["relativePath"]) + symbol_lines = symbol_file.split("\n") + symbol_body = "\n".join(symbol_lines[symbol_start_line:symbol_end_line+1]) + + # remove leading indentation + symbol_start_column = symbol["location"]["range"]["start"]["character"] + symbol_body = symbol_body[symbol_start_column:] + return symbol_body + + def request_parsed_files(self) -> list[str]: + """Retrieves relative paths of all files analyzed by the Language Server.""" + if not self.server_started: + self.logger.log( + "request_parsed_files called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + rel_file_paths = [] + for root, dirs, files in os.walk(self.repository_root_path): + dirs[:] = [d for d in dirs if not self.is_ignored_path(os.path.join(root, d))] + for file in files: + rel_file_path = os.path.relpath(os.path.join(root, file), start=self.repository_root_path) + if not self.is_ignored_path(rel_file_path): + rel_file_paths.append(rel_file_path) + return rel_file_paths + + def search_files_for_pattern( + self, + pattern: re.Pattern | str, + context_lines_before: int = 0, + context_lines_after: int = 0, + paths_include_glob: str | None = None, + paths_exclude_glob: str | None = None, + ) -> list[MatchedConsecutiveLines]: + """ + Search for a pattern across all files analyzed by the Language Server. + + :param pattern: Regular expression pattern to search for, either as a compiled Pattern or string + :param context_lines_before: Number of lines of context to include before each match + :param context_lines_after: Number of lines of context to include after each match + :param paths_include_glob: Glob pattern to filter which files to include in the search + :param paths_exclude_glob: Glob pattern to filter which files to exclude from the search. Takes precedence over paths_include_glob. + :return: List of matched consecutive lines with context + """ + if isinstance(pattern, str): + pattern = re.compile(pattern) + + relative_file_paths = self.request_parsed_files() + return search_files( + relative_file_paths, + pattern, + file_reader=self.retrieve_full_file_content, + context_lines_before=context_lines_before, + context_lines_after=context_lines_after, + paths_include_glob=paths_include_glob, + paths_exclude_glob=paths_exclude_glob + ) + + def request_referencing_symbols( + self, + relative_file_path: str, + line: int, + column: int, + include_imports: bool = True, + include_self: bool = False, + include_body: bool = False, + include_file_symbols: bool = False, + ) -> List[ReferenceInSymbol]: + """ + Finds all symbols that reference the symbol at the given location. + This is similar to request_references but filters to only include symbols + (functions, methods, classes, etc.) that reference the target symbol. + + :param relative_file_path: The relative path to the file. + :param line: The 0-indexed line number. + :param column: The 0-indexed column number. + :param include_imports: whether to also include imports as references. + Unfortunately, the LSP does not have an import type, so the references corresponding to imports + will not be easily distinguishable from definitions. + :param include_self: whether to include the references that is the "input symbol" itself. + Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition. + :param include_body: whether to include the body of the symbols in the result. + :param include_file_symbols: whether to include references that are file symbols. This + is often a fallback mechanism for when the reference cannot be resolved to a symbol. + :return: List of objects containing the symbol and the location of the reference. + """ + if not self.server_started: + self.logger.log( + "request_referencing_symbols called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + # First, get all references to the symbol + references = self.request_references(relative_file_path, line, column) + if not references: + return [] + + # For each reference, find the containing symbol + result = [] + incoming_symbol = None + for ref in references: + ref_path = ref["relativePath"] + ref_line = ref["range"]["start"]["line"] + ref_col = ref["range"]["start"]["character"] + + with self.open_file(ref_path) as file_data: + # Get the containing symbol for this reference + containing_symbol = self.request_containing_symbol( + ref_path, ref_line, ref_col, include_body=include_body + ) + if containing_symbol is None: + # TODO: HORRIBLE HACK! I don't know how to do it better for now... + # THIS IS BOUND TO BREAK IN MANY CASES! IT IS ALSO SPECIFIC TO PYTHON! + # Background: + # When a variable is used to change something, like + # + # instance = MyClass() + # instance.status = "new status" + # + # we can't find the containing symbol for the reference to `status` + # since there is no container on the line of the reference + # The hack is to try to find a variable symbol in the containing module + # by using the text of the reference to find the variable name (In a very heuristic way) + # and then look for a symbol with that name and kind Variable + ref_text = file_data.contents.split("\n")[ref_line] + if "." in ref_text: + containing_symbol_name = ref_text.split(".")[0] + all_symbols, _ = self.request_document_symbols(ref_path) + for symbol in all_symbols: + if symbol["name"] == containing_symbol_name and symbol["kind"] == multilspy_types.SymbolKind.Variable: + containing_symbol = copy(symbol) + containing_symbol["location"] = ref + containing_symbol["range"] = ref["range"] + break + + # We failed retrieving the symbol, falling back to creating a file symbol + if containing_symbol is None and include_file_symbols: + self.logger.log( + f"Could not find containing symbol for {ref_path}:{ref_line}:{ref_col}. Returning file symbol instead", + logging.WARNING + ) + fileRange = self._get_range_from_file_content(file_data.contents) + location = multilspy_types.Location( + uri=str(pathlib.Path(os.path.join(self.repository_root_path, ref_path)).as_uri()), + range=fileRange, + absolutePath=str(os.path.join(self.repository_root_path, ref_path)), + relativePath=ref_path, + ) + name = os.path.splitext(os.path.basename(ref_path))[0] + + if include_body: + body = self.retrieve_full_file_content(ref_path) + else: + body = "" + + containing_symbol = multilspy_types.UnifiedSymbolInformation( + kind=multilspy_types.SymbolKind.File, + range=fileRange, + selectionRange=fileRange, + location=location, + name=name, + children=[], + body=body, + ) + if containing_symbol is None or not include_file_symbols and containing_symbol["kind"] == multilspy_types.SymbolKind.File: + continue + + assert "location" in containing_symbol + assert "selectionRange" in containing_symbol + + # Checking for self-reference + if ( + containing_symbol["location"]["relativePath"] == relative_file_path + and containing_symbol["selectionRange"]["start"]["line"] == ref_line + and containing_symbol["selectionRange"]["start"]["character"] == ref_col + ): + incoming_symbol = containing_symbol + if include_self: + result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col)) + continue + else: + self.logger.log(f"Found self-reference for {incoming_symbol['name']}, skipping it since {include_self=}", logging.DEBUG) + continue + + # checking whether reference is an import + # This is neither really safe nor elegant, but if we don't do it, + # there is no way to distinguish between definitions and imports as import is not a symbol-type + # and we get the type referenced symbol resulting from imports... + if (not include_imports \ + and incoming_symbol is not None \ + and containing_symbol["name"] == incoming_symbol["name"] \ + and containing_symbol["kind"] == incoming_symbol["kind"] \ + ): + self.logger.log( + f"Found import of referenced symbol {incoming_symbol['name']}" + f"in {containing_symbol['location']['relativePath']}, skipping", + logging.DEBUG + ) + continue + + result.append(ReferenceInSymbol(symbol=containing_symbol, line=ref_line, character=ref_col)) + + return result + + def request_containing_symbol( + self, + relative_file_path: str, + line: int, + column: Optional[int] = None, + strict: bool = False, + include_body: bool = False, + ) -> multilspy_types.UnifiedSymbolInformation | None: + """ + Finds the first symbol containing the position for the given file. + For Python, container symbols are considered to be those with kinds corresponding to + functions, methods, or classes (typically: Function (12), Method (6), Class (5)). + + The method operates as follows: + - Request the document symbols for the file. + - Filter symbols to those that start at or before the given line. + - From these, first look for symbols whose range contains the (line, column). + - If one or more symbols contain the position, return the one with the greatest starting position + (i.e. the innermost container). + - If none (strictly) contain the position, return the symbol with the greatest starting position + among those above the given line. + - If no container candidates are found, return None. + + :param relative_file_path: The relative path to the Python file. + :param line: The 0-indexed line number. + :param column: The 0-indexed column (also called character). If not passed, the lookup will be based + only on the line. + :param strict: If True, the position must be strictly within the range of the symbol. + Setting to True is useful for example for finding the parent of a symbol, as with strict=False, + and the line pointing to a symbol itself, the containing symbol will be the symbol itself + (and not the parent). + :param include_body: Whether to include the body of the symbol in the result. + :return: The container symbol (if found) or None. + """ + # checking if the line is empty, unfortunately ugly and duplicating code, but I don't want to refactor + with self.open_file(relative_file_path): + absolute_file_path = str( + PurePath(self.repository_root_path, relative_file_path) + ) + content = FileUtils.read_file(self.logger, absolute_file_path) + if content.split("\n")[line].strip() == "": + self.logger.log( + f"Passing empty lines to request_container_symbol is currently not supported, {relative_file_path=}, {line=}", + logging.ERROR, + ) + return None + + symbols, _ = self.request_document_symbols(relative_file_path) + + # make jedi and pyright api compatible + # the former has no location, the later has no range + # we will just always add location of the desired format to all symbols + for symbol in symbols: + if "location" not in symbol: + range = symbol["range"] + location = multilspy_types.Location( + uri=f"file:/{absolute_file_path}", + range=range, + absolutePath=absolute_file_path, + relativePath=relative_file_path, + ) + symbol["location"] = location + else: + location = symbol["location"] + assert "range" in location + location["absolutePath"] = absolute_file_path + location["relativePath"] = relative_file_path + location["uri"] = Path(absolute_file_path).as_uri() + + # Allowed container kinds, currently only for Python + container_symbol_kinds = { + multilspy_types.SymbolKind.Method, + multilspy_types.SymbolKind.Function, + multilspy_types.SymbolKind.Class + } + + def is_position_in_range(line: int, range_d: multilspy_types.Range) -> bool: + start = range_d["start"] + end = range_d["end"] + + column_condition = True + if strict: + line_condition = end["line"] >= line > start["line"] + if column is not None and line == start["line"]: + column_condition = column > start["character"] + else: + line_condition = end["line"] >= line >= start["line"] + if column is not None and line == start["line"]: + column_condition = column >= start["character"] + return line_condition and column_condition + + # Only consider containers that are not one-liners (otherwise we may get imports) + candidate_containers = [ + s for s in symbols if s["kind"] in container_symbol_kinds and s["location"]["range"]["start"]["line"] != s["location"]["range"]["end"]["line"] + ] + var_containers = [ + s for s in symbols if s["kind"] == multilspy_types.SymbolKind.Variable + ] + candidate_containers.extend(var_containers) + + if not candidate_containers: + return None + + # From the candidates, find those whose range contains the given position. + containing_symbols = [] + for symbol in candidate_containers: + s_range = symbol["location"]["range"] + if not is_position_in_range(line, s_range): + continue + containing_symbols.append(symbol) + + if containing_symbols: + # Return the one with the greatest starting position (i.e. the innermost container). + containing_symbol = max(containing_symbols, key=lambda s: s["location"]["range"]["start"]["line"]) + if include_body: + containing_symbol["body"] = self.retrieve_symbol_body(containing_symbol) + return containing_symbol + else: + return None + + def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None: + """ + Finds the container of the given symbol if there is one. If the parent attribute is present, the parent is returned + without further searching. + + :param symbol: The symbol to find the container of. + :param include_body: whether to include the body of the symbol in the result. + :return: The container of the given symbol or None if no container is found. + """ + if "parent" in symbol: + return symbol["parent"] + assert "location" in symbol, f"Symbol {symbol} has no location and no parent attribute" + return self.request_containing_symbol( + symbol["location"]["relativePath"], + symbol["location"]["range"]["start"]["line"], + symbol["location"]["range"]["start"]["character"], + strict=True, + include_body=include_body, + ) + + def request_defining_symbol( + self, + relative_file_path: str, + line: int, + column: int, + include_body: bool = False, + ) -> Optional[multilspy_types.UnifiedSymbolInformation]: + """ + Finds the symbol that defines the symbol at the given location. + + This method first finds the definition of the symbol at the given position, + then retrieves the full symbol information for that definition. + + :param relative_file_path: The relative path to the file. + :param line: The 0-indexed line number. + :param column: The 0-indexed column number. + :param include_body: whether to include the body of the symbol in the result. + :return: The symbol information for the definition, or None if not found. + """ + if not self.server_started: + self.logger.log( + "request_defining_symbol called before Language Server started", + logging.ERROR, + ) + raise MultilspyException("Language Server not started") + + # Get the definition location(s) + definitions = self.request_definition(relative_file_path, line, column) + if not definitions: + return None + + # Use the first definition location + definition = definitions[0] + def_path = definition["relativePath"] + def_line = definition["range"]["start"]["line"] + def_col = definition["range"]["start"]["character"] + + # Find the symbol at or containing this location + 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: + """ + The path to the cache file for the document symbols. + """ + return Path(self.repository_root_path) / ".serena" / "cache" / self.language_id / "document_symbols_cache_v20-05-25.pkl" + + def index_repository(self, progress_bar: bool = True, save_after_n_files: int = 10) -> None: + """Will go through the entire repository and "index" all files, meaning save their symbols to the cache. + + :param progress_bar: Whether to show a progress bar while indexing the repository. + :param save_after_n_files: How many files to process before saving a checkpoint of the cache. + """ + parsed_files = self.request_parsed_files() + files_processed = 0 + pbar = tqdm.tqdm(parsed_files, disable=not progress_bar) + for relative_file_path in pbar: + pbar.set_description(f"Indexing ({os.path.basename(relative_file_path)})") + self.request_document_symbols(relative_file_path, include_body=False) + self.request_document_symbols(relative_file_path, include_body=True) + files_processed += 1 + if files_processed % save_after_n_files == 0: + self.save_cache() + self.save_cache() + + def save_cache(self): + with self._cache_lock: + if not self._cache_has_changed: + self.logger.log("No changes to document symbols cache, skipping save", logging.DEBUG) + return + + self.logger.log(f"Saving updated document symbols cache to {self.cache_path}", logging.INFO) + self.cache_path.parent.mkdir(parents=True, exist_ok=True) + try: + with open(self.cache_path, "wb") as f: + pickle.dump(self._document_symbols_cache, f) + self._cache_has_changed = False + except Exception as e: + self.logger.log( + f"Failed to save document symbols cache to {self.cache_path}: {e}. " + "Note: this may have resulted in a corrupted cache file.", + logging.ERROR, + ) + + def load_cache(self): + if not self.cache_path.exists(): + return + + with self._cache_lock: + self.logger.log(f"Loading document symbols cache from {self.cache_path}", logging.INFO) + try: + with open(self.cache_path, "rb") as f: + self._document_symbols_cache = pickle.load(f) + self.logger.log(f"Loaded {len(self._document_symbols_cache)} document symbols from cache.", logging.INFO) + except Exception as e: + # cache often becomes corrupt, so just skip loading it + self.logger.log( + f"Failed to load document symbols cache from {self.cache_path}: {e}. Possible cause: the cache file is corrupted. " + "Check for any errors related to saving the cache in the logs.", + logging.ERROR, + ) + + def request_workspace_symbol(self, query: str) -> Union[List[multilspy_types.UnifiedSymbolInformation], None]: + """ + Raise a [workspace/symbol](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol) request to the Language Server + to find symbols across the whole workspace. Wait for the response and return the result. + + :param query: The query string to filter symbols by + + :return: A list of matching symbols + """ + response = self.server.send.workspace_symbol({"query": query}) + if response is None: + return None + + assert isinstance(response, list) + + ret: List[multilspy_types.UnifiedSymbolInformation] = [] + for item in response: + assert isinstance(item, dict) + + assert LSPConstants.NAME in item + assert LSPConstants.KIND in item + assert LSPConstants.LOCATION in item + + ret.append(multilspy_types.UnifiedSymbolInformation(**item)) + + return ret + + def start(self) -> "SolidLanguageServer": + """ + Starts the language server process and connects to it. Call shutdown when ready. + + :return: self for method chaining + """ + self.logger.log(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}", logging.INFO) + self._server_context = self._start_server_process() + return self + + def stop(self, shutdown_timeout: float = 2.0) -> None: + self._shutdown(timeout=shutdown_timeout) + + @property + def language_server(self) -> Self: + return self + + def is_running(self) -> bool: + return self.server.is_running() diff --git a/src/solidlsp/ls_handler.py b/src/solidlsp/ls_handler.py new file mode 100644 index 00000000..b34571f9 --- /dev/null +++ b/src/solidlsp/ls_handler.py @@ -0,0 +1,513 @@ +import asyncio +import json +import logging +import os +import subprocess +import threading +import time +from dataclasses import dataclass +from queue import Queue +from typing import Any, Callable, Dict, Optional + +import psutil + +from multilspy.lsp_protocol_handler.lsp_requests import LspNotification +from multilspy.lsp_protocol_handler.lsp_types import ErrorCodes +from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo, StringDict, content_length, ENCODING, Error, MessageType, \ + make_notification, PayloadLike, make_response, make_error_response, make_request, create_message +from multilspy.multilspy_exceptions import MultilspyException +from solidlsp.lsp_request import SolidLspRequest + +log = logging.getLogger(__name__) + + +class Request: + + @dataclass + class Result: + payload: Optional[PayloadLike] = None + error: Optional[Error] = None + + def is_error(self) -> bool: + return self.error is not None + + def __init__(self) -> None: + self._result_queue = Queue() + + def on_result(self, params: PayloadLike) -> None: + self._result_queue.put(Request.Result(payload=params)) + + def on_error(self, err: Error) -> None: + self._result_queue.put(Request.Result(error=err)) + + def get_result(self) -> Result: + # TODO could add timeout + return self._result_queue.get() + + +class SolidLanguageServerHandler: + """ + This class provides the implementation of Python client for the Language Server Protocol. + A class that launches the language server and communicates with it + using the Language Server Protocol (LSP). + + It provides methods for sending requests, responses, and notifications to the server + and for registering handlers for requests and notifications from the server. + + Uses JSON-RPC 2.0 for communication with the server over stdin/stdout. + + Attributes: + send: A LspRequest object that can be used to send requests to the server and + await for the responses. + 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 + 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. + on_notification_handlers: A dictionary that maps method names to callback functions + that handle notifications from the server. + logger: An optional function that takes two strings (source and destination) and + a payload dictionary, and logs the communication between the client and the server. + tasks: A dictionary that maps task ids to asyncio.Task objects that represent + the asynchronous tasks created by the handler. + task_counter: An integer that represents the next available task id for the handler. + loop: An asyncio.AbstractEventLoop object that represents the event loop used by the handler. + start_independent_lsp_process: An optional boolean flag that indicates whether to start the + language server process in an independent process group. Default is `True`. Setting it to + `False` means that the language server process will be in the same process group as the + the current process, and any SIGINT and SIGTERM signals will be sent to both processes. + """ + + def __init__( + self, + process_launch_info: ProcessLaunchInfo, + logger: Optional[Callable[[str, str, StringDict | str], None]] = None, + start_independent_lsp_process=True, + ) -> None: + """ + Params: + cmd: A string that represents the command to launch the language server process. + logger: An optional function that takes two strings (source and destination) and + a payload dictionary, and logs the communication between the client and the server. + """ + self.send = SolidLspRequest(self.send_request) + self.notify = LspNotification(self.send_notification) + + self.process_launch_info = process_launch_info + self.process = None + self._received_shutdown = False + + self.request_id = 1 + self._response_handlers: Dict[Any, Request] = {} + self.on_request_handlers = {} + self.on_notification_handlers = {} + self.logger = logger + self.tasks = {} + self.task_counter = 0 + self.loop = None + self.start_independent_lsp_process = start_independent_lsp_process + + # Add thread locks for shared resources to prevent race conditions + self._stdin_lock = threading.Lock() + self._request_id_lock = threading.Lock() + self._response_handlers_lock = threading.Lock() + self._tasks_lock = threading.Lock() + + def is_running(self) -> bool: + """ + Checks if the language server process is currently running. + """ + return self.process is not None and self.process.returncode is None + + def start(self) -> None: + """ + Starts the language server process and creates a task to continuously read from its stdout to handle communications + from the server to the client + """ + child_proc_env = os.environ.copy() + child_proc_env.update(self.process_launch_info.env) + + log.info("Starting language server process via command: %s", self.process_launch_info.cmd) + self.process = subprocess.Popen( + self.process_launch_info.cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + env=child_proc_env, + cwd=self.process_launch_info.cwd, + start_new_session=self.start_independent_lsp_process, + shell=True + ) + + # Check if process terminated immediately + if self.process.returncode is not None: + log.error("Language server has already terminated/could not be started") + # Process has already terminated + stderr_data = self.process.stderr.read() + error_message = stderr_data.decode('utf-8', errors='replace') + raise RuntimeError(f"Process terminated immediately with code {self.process.returncode}. Error: {error_message}") + + # start threads to read stdout and stderr of the process + threading.Thread( + target=self.run_forever, + name="LSP stdout reader", + daemon=True, + ).start() + threading.Thread( + target=self.run_forever_stderr, + name="LSP stderr reader", + daemon=True, + ).start() + + def stop(self) -> None: + """ + Sends the terminate signal to the language server process and waits for it to exit, with a timeout, killing it if necessary + """ + process = self.process + self.process = None + if process: + self._cleanup_process(process) + + def _cleanup_process(self, process): + """Clean up a process: close stdin, terminate/kill process, close stdout/stderr.""" + # Close stdin first to prevent deadlocks + # See: https://bugs.python.org/issue35539 + self._safely_close_pipe(process.stdin) + + # Terminate/kill the process if it's still running + if process.returncode is None: + self._terminate_or_kill_process(process) + + # Close stdout and stderr pipes after process has exited + # This is essential to prevent "I/O operation on closed pipe" errors and + # "Event loop is closed" errors during garbage collection + # See: https://bugs.python.org/issue41320 and https://github.com/python/cpython/issues/88050 + self._safely_close_pipe(process.stdout) + self._safely_close_pipe(process.stderr) + + def _safely_close_pipe(self, pipe): + """Safely close a pipe, ignoring any exceptions.""" + if pipe: + try: + pipe.close() + except Exception: + pass + + def _terminate_or_kill_process(self, process): + """Try to terminate the process gracefully, then forcefully if necessary.""" + # 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" + + # Try to get the parent process + parent = None + try: + parent = psutil.Process(process.pid) + except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): + pass + + # If we have the parent process and it's running, signal the entire tree + if parent and parent.is_running(): + # Signal children first + for child in parent.children(recursive=True): + try: + getattr(child, signal_method)() + except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): + pass + + # Then signal the parent + try: + getattr(parent, signal_method)() + except (psutil.NoSuchProcess, psutil.AccessDenied, Exception): + pass + else: + # Fall back to direct process signaling + try: + getattr(process, signal_method)() + except Exception: + pass + + def shutdown(self) -> None: + """ + Perform the shutdown sequence for the client, including sending the shutdown request to the server and notifying it of exit + """ + 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: + """ + Create a log message + """ + if self.logger is not None: + self.logger("client", "logger", message) + + @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 + continue + data += chunk + return data + + def run_forever(self) -> bool: + """ + Continuously read from the language server process stdout and handle the messages + invoking the registered response and notification handlers + """ + try: + while self.process and self.process.stdout and self.process.stdout.readable(): + line = self.process.stdout.readline() + if not line: + continue + try: + num_bytes = content_length(line) + except ValueError: + continue + if num_bytes is None: + continue + while line and line.strip(): + line = self.process.stdout.readline() + if not line: + continue + 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 + + def run_forever_stderr(self) -> None: + """ + Continuously read from the language server process stderr and log the messages + """ + try: + while self.process and self.process.stderr and self.process.stderr.readable(): + line = self.process.stderr.readline() + if not line: + continue + self._log("LSP stderr: " + line.decode(ENCODING, errors='replace')) + except (BrokenPipeError, ConnectionResetError): + pass + + def _handle_body(self, body: bytes) -> None: + """ + Parse the body text received from the language server process and invoke the appropriate handler + """ + try: + self._receive_payload(json.loads(body)) + except IOError as ex: + self._log(f"malformed {ENCODING}: {ex}") + except UnicodeDecodeError as ex: + self._log(f"malformed {ENCODING}: {ex}") + except json.JSONDecodeError as ex: + self._log(f"malformed JSON: {ex}") + + def _receive_payload(self, payload: StringDict) -> None: + """ + Determine if the payload received from server is for a request, response, or notification and invoke the appropriate handler + """ + if self.logger: + self.logger("server", "client", payload) + try: + if "method" in payload: + if "id" in payload: + self._request_handler(payload) + else: + self._notification_handler(payload) + elif "id" in payload: + self._response_handler(payload) + else: + self._log(f"Unknown payload type: {payload}") + except Exception as err: + self._log(f"Error handling server payload: {err}") + + def send_notification(self, method: str, params: Optional[dict] = None) -> None: + """ + Send notification pertaining to the given method to the server with the given parameters + """ + self._send_payload(make_notification(method, params)) + + def send_response(self, request_id: Any, params: PayloadLike) -> None: + """ + Send response to the given request id to the server with the given parameters + """ + self._send_payload(make_response(request_id, params)) + + def send_error_response(self, request_id: Any, err: Error) -> 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 send_request(self, method: str, params: Optional[dict] = 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 + + with self._response_handlers_lock: + self._response_handlers[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() + + self._log(f"Processing result") + if result.is_error(): + raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {result.error}") from result.error + + self._log(f"Returning non-error result, which is:\n{result.payload}") + return result.payload + + def _send_payload(self, payload: StringDict) -> None: + """ + Send the payload to the server by writing to its stdin asynchronously. + """ + if not self.process or not self.process.stdin: + return + self._log(payload) + msg = create_message(payload) + + # Use lock to prevent concurrent writes to stdin that cause buffer corruption + with self._stdin_lock: + try: + self.process.stdin.writelines(msg) + self.process.stdin.flush() + except (BrokenPipeError, ConnectionResetError, OSError) as e: + # Log the error but don't raise to prevent cascading failures + if self.logger: + self.logger("client", "logger", f"Failed to write to stdin: {e}") + return + + def on_request(self, method: str, cb) -> None: + """ + Register the callback function to handle requests from the server to the client for the given method + """ + self.on_request_handlers[method] = cb + + def on_notification(self, method: str, cb) -> None: + """ + Register the callback function to handle notifications from the server to the client for the given method + """ + self.on_notification_handlers[method] = cb + + def _response_handler(self, response: StringDict) -> None: + """ + 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"]) + + 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"])) + else: + request.on_error(Error(ErrorCodes.InvalidRequest, "")) + + def _request_handler(self, response: StringDict) -> None: + """ + Handle the request received from the server: call the appropriate callback function and return the result + """ + method = response.get("method", "") + params = response.get("params") + request_id = response.get("id") + handler = self.on_request_handlers.get(method) + if not handler: + self.send_error_response( + request_id, + Error( + ErrorCodes.MethodNotFound, + "method '{}' not handled on client.".format(method), + ), + ) + return + try: + self.send_response(request_id, handler(params)) + except Error as ex: + self.send_error_response(request_id, ex) + except Exception as ex: + self.send_error_response(request_id, Error(ErrorCodes.InternalError, str(ex))) + + def _notification_handler(self, response: StringDict) -> None: + """ + Handle the notification received from the server: call the appropriate callback function + """ + method = response.get("method", "") + params = response.get("params") + handler = self.on_notification_handlers.get(method) + if not handler: + self._log(f"unhandled {method}") + return + try: + handler(params) + except asyncio.CancelledError: + return + except Exception as ex: + if (not self._received_shutdown) and self.logger: + self.logger( + "client", + "logger", + str( + { + "type": MessageType.error, + "message": str(ex), + "method": method, + "params": params, + } + ), + ) diff --git a/src/solidlsp/lsp_request.py b/src/solidlsp/lsp_request.py new file mode 100644 index 00000000..984407b5 --- /dev/null +++ b/src/solidlsp/lsp_request.py @@ -0,0 +1,435 @@ +from typing import List, Union +from multilspy.lsp_protocol_handler import lsp_types + + +class SolidLspRequest: + def __init__(self, send_request): + self.send_request = send_request + + 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 + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Definition} or a + Thenable that resolves to such.""" + return self.send_request("textDocument/implementation", params) + + def type_definition( + self, params: lsp_types.TypeDefinitionParams + ) -> Union["lsp_types.Definition", List["lsp_types.LocationLink"], None]: + """A request to resolve the type definition locations of a symbol at a given text + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Definition} or a + Thenable that resolves to such.""" + 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 + parameter is of type {@link DocumentColorParams} the + response is of type {@link ColorInformation ColorInformation[]} or a Thenable + that resolves to such.""" + 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 + parameter is of type {@link ColorPresentationParams} the + response is of type {@link ColorInformation ColorInformation[]} or a Thenable + that resolves to such.""" + return self.send_request("textDocument/colorPresentation", params) + + def folding_range( + self, params: lsp_types.FoldingRangeParams + ) -> Union[List["lsp_types.FoldingRange"], None]: + """A request to provide folding ranges in a document. The request's + parameter is of type {@link FoldingRangeParams}, the + response is of type {@link FoldingRangeList} or a Thenable + that resolves to such.""" + 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 + document position. The request's parameter is of type [TextDocumentPositionParams] + (#TextDocumentPositionParams) the response is of type {@link Declaration} + or a typed array of {@link DeclarationLink} or a Thenable that resolves + to such.""" + return self.send_request("textDocument/declaration", params) + + def selection_range( + self, params: lsp_types.SelectionRangeParams + ) -> Union[List["lsp_types.SelectionRange"], None]: + """A request to provide selection ranges in a document. The request's + parameter is of type {@link SelectionRangeParams}, the + response is of type {@link SelectionRange SelectionRange[]} or a Thenable + that resolves to such.""" + return self.send_request("textDocument/selectionRange", params) + + def prepare_call_hierarchy( + self, params: lsp_types.CallHierarchyPrepareParams + ) -> Union[List["lsp_types.CallHierarchyItem"], None]: + """A request to result a `CallHierarchyItem` in a document at a given position. + Can be used as an input to an incoming or outgoing call hierarchy. + + @since 3.16.0""" + return self.send_request("textDocument/prepareCallHierarchy", params) + + def incoming_calls( + self, params: lsp_types.CallHierarchyIncomingCallsParams + ) -> Union[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) + + def outgoing_calls( + self, params: lsp_types.CallHierarchyOutgoingCallsParams + ) -> Union[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) + + 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) + + 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) + + 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) + + 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) + + 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 + created as long as the creation is triggered from within the client. + + @since 3.16.0""" + 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 + renamed as long as the rename is triggered from within the client. + + @since 3.16.0""" + 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 + files were deleted from within the client. + + @since 3.16.0""" + return self.send_request("workspace/willDeleteFiles", params) + + def moniker( + self, params: lsp_types.MonikerParams + ) -> Union[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) + + def prepare_type_hierarchy( + self, params: lsp_types.TypeHierarchyPrepareParams + ) -> Union[List["lsp_types.TypeHierarchyItem"], None]: + """A request to result a `TypeHierarchyItem` in a document at a given position. + Can be used as an input to a subtypes or supertypes type hierarchy. + + @since 3.17.0""" + return self.send_request("textDocument/prepareTypeHierarchy", params) + + def type_hierarchy_supertypes( + self, params: lsp_types.TypeHierarchySupertypesParams + ) -> Union[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) + + def type_hierarchy_subtypes( + self, params: lsp_types.TypeHierarchySubtypesParams + ) -> Union[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) + + def inline_value( + self, params: lsp_types.InlineValueParams + ) -> Union[List["lsp_types.InlineValue"], None]: + """A request to provide inline values in a document. The request's parameter is of + type {@link InlineValueParams}, the response is of type + {@link InlineValue InlineValue[]} or a Thenable that resolves to such. + + @since 3.17.0""" + return self.send_request("textDocument/inlineValue", params) + + def inlay_hint( + self, params: lsp_types.InlayHintParams + ) -> Union[List["lsp_types.InlayHint"], None]: + """A request to provide inlay hints in a document. The request's parameter is of + type {@link InlayHintsParams}, the response is of type + {@link InlayHint InlayHint[]} or a Thenable that resolves to such. + + @since 3.17.0""" + 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. + The request's parameter is of type {@link InlayHint}, the response is + of type {@link InlayHint} or a Thenable that resolves to such. + + @since 3.17.0""" + 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) + + 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) + + def initialize( + self, params: lsp_types.InitializeParams + ) -> "lsp_types.InitializeResult": + """The initialize request is sent from the client to the server. + It is sent once as the request after starting up the server. + The requests parameter is of type {@link InitializeParams} + the response if of type {@link InitializeResult} of a Thenable that + resolves to such.""" + return self.send_request("initialize", params) + + def shutdown(self) -> None: + """A shutdown request is sent from the client to the server. + It is sent once when the client decides to shutdown the + server. The only notification that is sent after a shutdown request + is the exit event.""" + return self.send_request("shutdown") + + def will_save_wait_until( + self, params: lsp_types.WillSaveTextDocumentParams + ) -> Union[List["lsp_types.TextEdit"], None]: + """A document will save request is sent from the client to the server before + the document is actually saved. The request can return an array of TextEdits + which will be applied to the text document before it is saved. Please note that + clients might drop results if computing the text edits took too long or if a + server constantly fails on this request. This is done to keep the save fast and + reliable.""" + 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 + parameter is of type {@link TextDocumentPosition} the response + is of type {@link CompletionItem CompletionItem[]} or {@link CompletionList} + or a Thenable that resolves to such. + + The request can delay the computation of the {@link CompletionItem.detail `detail`} + and {@link CompletionItem.documentation `documentation`} properties to the `completionItem/resolve` + 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) + + 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) + + 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) + + def signature_help( + self, params: lsp_types.SignatureHelpParams + ) -> Union["lsp_types.SignatureHelp", None]: + 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 + document position. The request's parameter is of type [TextDocumentPosition] + (#TextDocumentPosition) the response is of either type {@link Definition} + or a typed array of {@link DefinitionLink} or a Thenable that resolves + to such.""" + return self.send_request("textDocument/definition", params) + + def references( + self, params: lsp_types.ReferenceParams + ) -> Union[List["lsp_types.Location"], None]: + """A request to resolve project-wide references for the symbol denoted + by the given text document position. The request's parameter is of + 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) + + def document_highlight( + self, params: lsp_types.DocumentHighlightParams + ) -> Union[List["lsp_types.DocumentHighlight"], None]: + """Request to resolve a {@link DocumentHighlight} for a given + text document position. The request's parameter is of type [TextDocumentPosition] + (#TextDocumentPosition) the request response is of type [DocumentHighlight[]] + (#DocumentHighlight) or a Thenable that resolves to such.""" + return self.send_request("textDocument/documentHighlight", params) + + def document_symbol( + self, params: lsp_types.DocumentSymbolParams + ) -> Union[ + List["lsp_types.SymbolInformation"], List["lsp_types.DocumentSymbol"], None + ]: + """A request to list all symbols found in a given text document. The request's + parameter is of type {@link TextDocumentIdentifier} the + response is of type {@link SymbolInformation SymbolInformation[]} or a Thenable + that resolves to such.""" + return self.send_request("textDocument/documentSymbol", params) + + def code_action( + self, params: lsp_types.CodeActionParams + ) -> Union[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) + + 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) + + def workspace_symbol( + self, params: lsp_types.WorkspaceSymbolParams + ) -> Union[ + List["lsp_types.SymbolInformation"], List["lsp_types.WorkspaceSymbol"], None + ]: + """A request to list project-wide symbols matching the query string given + by the {@link WorkspaceSymbolParams}. The response is + of type {@link SymbolInformation SymbolInformation[]} or a Thenable that + resolves to such. + + @since 3.17.0 - support for WorkspaceSymbol in the returned data. Clients + need to advertise support for WorkspaceSymbols via the client capability + `workspace.symbol.resolveSupport`. + """ + 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 + symbol's location. + + @since 3.17.0""" + return self.send_request("workspaceSymbol/resolve", params) + + def code_lens( + self, params: lsp_types.CodeLensParams + ) -> Union[List["lsp_types.CodeLens"], None]: + """A request to provide code lens for the given text document.""" + 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) + + def document_link( + self, params: lsp_types.DocumentLinkParams + ) -> Union[List["lsp_types.DocumentLink"], None]: + """A request to provide document links""" + 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) + + def formatting( + self, params: lsp_types.DocumentFormattingParams + ) -> Union[List["lsp_types.TextEdit"], None]: + """A request to to format a whole document.""" + return self.send_request("textDocument/formatting", params) + + def range_formatting( + self, params: lsp_types.DocumentRangeFormattingParams + ) -> Union[List["lsp_types.TextEdit"], None]: + """A request to to format a range in a document.""" + return self.send_request("textDocument/rangeFormatting", params) + + def on_type_formatting( + self, params: lsp_types.DocumentOnTypeFormattingParams + ) -> Union[List["lsp_types.TextEdit"], None]: + """A request to format a document on type.""" + 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) + + 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) + + 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) diff --git a/test/conftest.py b/test/conftest.py index 994034e2..75bc8c74 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,6 @@ import logging from pathlib import Path +from typing import cast import pytest from sensai.util.logging import configure @@ -7,7 +8,9 @@ from sensai.util.logging import configure from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from serena.constants import USE_SOLID_LSP from serena.util.file_system import GitignoreParser +from solidlsp.ls import SolidLanguageServer configure(level=logging.DEBUG) @@ -43,7 +46,11 @@ def create_ls( config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) logger = MultilspyLogger(log_level=log_level) - return SyncLanguageServer.create(config, logger, repo_path) + if USE_SOLID_LSP: + ls = SolidLanguageServer.create(config, logger, repo_path) + return cast(SyncLanguageServer, ls) # TODO: Fix type + else: + return SyncLanguageServer.create(config, logger, repo_path) def create_default_ls(language: Language) -> SyncLanguageServer: diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py index fb13f866..eb9b3f06 100644 --- a/test/serena/test_make_tool_process_isolation.py +++ b/test/serena/test_make_tool_process_isolation.py @@ -4,10 +4,12 @@ import pytest from mcp.server.fastmcp.tools.base import Tool as MCPTool from serena.agent import SerenaAgent, ToolRegistry -from serena.mcp import make_tool +from serena.mcp import SerenaMCPFactory from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool from test.serena.test_serena_agent import SerenaConfigForTests +make_tool = SerenaMCPFactory.make_mcp_tool + @pytest.fixture def in_memory_config(): diff --git a/test/serena/test_mcp.py b/test/serena/test_mcp.py index eed27a7f..8ffb2c21 100644 --- a/test/serena/test_mcp.py +++ b/test/serena/test_mcp.py @@ -4,7 +4,9 @@ import pytest from mcp.server.fastmcp.tools.base import Tool as MCPTool from serena.agent import Tool, ToolRegistry -from serena.mcp import make_tool +from serena.mcp import SerenaMCPFactory + +make_tool = SerenaMCPFactory.make_mcp_tool class BaseMockTool(Tool): diff --git a/uv.lock b/uv.lock index 05834d9c..c7318117 100644 --- a/uv.lock +++ b/uv.lock @@ -101,6 +101,15 @@ jupyter = [ { name = "tokenize-rt" }, ] +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -210,17 +219,20 @@ wheels = [ ] [[package]] -name = "fastapi" -version = "0.115.12" +name = "flask" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/de/e47735752347f4128bcf354e0da07ef311a78244eba9e3dc1d4a5ab21a98/flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e", size = 753440 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, + { url = "https://files.pythonhosted.org/packages/3d/68/9d4508e893976286d2ead7f8f571314af6c2037af34853a30fd769c02e9d/flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c", size = 103305 }, ] [[package]] @@ -394,6 +406,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + [[package]] name = "jedi" version = "0.19.2" @@ -959,7 +980,7 @@ source = { editable = "." } dependencies = [ { name = "docstring-parser" }, { name = "dotenv" }, - { name = "fastapi" }, + { name = "flask" }, { name = "jinja2" }, { name = "joblib" }, { name = "mcp" }, @@ -1007,7 +1028,7 @@ requires-dist = [ { name = "black", extras = ["jupyter"], marker = "extra == 'dev'", specifier = ">=23.7.0" }, { name = "docstring-parser", specifier = ">=0.16" }, { name = "dotenv", specifier = ">=0.9.9" }, - { name = "fastapi", specifier = ">=0.115.12" }, + { name = "flask", specifier = ">=3.0.0" }, { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.8.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, @@ -1299,3 +1320,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +]