Merge pull request #211 from oraios/solid-lsp

Add fully synchronous language server implementation (solidlsp)
This commit is contained in:
Michael Panchenko authored and GitHub committed 2025-06-22 12:00:51 +02:00
commit a98844f9bd
52 files changed
+11573 -212

No files matched your search

+1
View File
@@ -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/**
+161
View File
@@ -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.
+11
View File
@@ -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!**
+5 -5
View File
@@ -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",
+11 -8
View File
@@ -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
+34 -14
View File
@@ -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 <project_name> or --project <project_root>."
@@ -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}"
)
+6
View File
@@ -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).
"""
+45 -13
View File
@@ -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/<path:filename>")
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]:
+316 -142
View File
@@ -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)
+8 -17
View File
@@ -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:
+21
View File
@@ -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
+19 -1
View File
@@ -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
@@ -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()
@@ -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"
}
]
}
@@ -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"
}
]
}
@@ -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({})
@@ -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"
}
]
}
@@ -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"
}
]
}
@@ -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()
@@ -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 ]"
}
@@ -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"
}
}
}
@@ -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()
@@ -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"
}
]
}
@@ -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"
}
]
}
@@ -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)
@@ -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"
}
]
}
@@ -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/<id>": "${env:USERPROFILE}/.rustup/toolchains/<toolchain-id>/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"
}
]
}
@@ -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({})
@@ -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 ]"
}
@@ -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()
@@ -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"
}
}
}
@@ -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"
}
]
}
@@ -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()
@@ -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"
}
]
}
@@ -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
}
}
@@ -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()
@@ -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/<id>": "${env:USERPROFILE}/.rustup/toolchains/<toolchain-id>/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"
}
]
}
@@ -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"
}
]
}
@@ -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()
@@ -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"
}
]
}
@@ -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"
}
]
}
@@ -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()
@@ -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/<id>": "${env:USERPROFILE}/.rustup/toolchains/<toolchain-id>/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"
}
]
}
@@ -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"
}
]
}
@@ -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)
+1635
View File
File diff suppressed because it is too large. Load diff
+513
View File
@@ -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,
}
),
)
+435
View File
@@ -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)
+8 -1
View File
@@ -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:
@@ -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():
+3 -1
View File
@@ -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):
Generated
+42 -9
View File
@@ -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 },
]