mirror of
https://github.com/tiennm99/serena.git
synced 2026-09-03 08:19:19 +00:00
Adapt Pyright and Intelephense servers to solidlsp
This commit is contained in:
@@ -2,27 +2,25 @@
|
||||
Provides PHP specific instantiation of the LanguageServer class using Intelephense.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import pathlib
|
||||
from contextlib import asynccontextmanager
|
||||
import shutil
|
||||
import subprocess
|
||||
from time import sleep
|
||||
from typing import AsyncIterator
|
||||
|
||||
from overrides import override
|
||||
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
from multilspy.language_server import LanguageServer
|
||||
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
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(LanguageServer):
|
||||
|
||||
class Intelephense(SolidLanguageServer):
|
||||
"""
|
||||
Provides PHP specific instantiation of the LanguageServer class using Intelephense.
|
||||
"""
|
||||
@@ -109,7 +107,6 @@ class Intelephense(LanguageServer):
|
||||
ProcessLaunchInfo(cmd=intelephense_cmd, cwd=repository_root_path),
|
||||
"php"
|
||||
)
|
||||
self.server_ready = asyncio.Event()
|
||||
self.request_id = 0
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
@@ -136,16 +133,15 @@ class Intelephense(LanguageServer):
|
||||
|
||||
return d
|
||||
|
||||
@asynccontextmanager
|
||||
async def start_server(self) -> AsyncIterator["Intelephense"]:
|
||||
def _start_server(self):
|
||||
"""Start Intelephense server process"""
|
||||
async def register_capability_handler(params):
|
||||
def register_capability_handler(params):
|
||||
return
|
||||
|
||||
async def window_log_message(msg):
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
async def do_nothing(params):
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
@@ -153,48 +149,44 @@ class Intelephense(LanguageServer):
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
|
||||
async with super().start_server():
|
||||
self.logger.log("Starting Intelephense server process", logging.INFO)
|
||||
await self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
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 = await 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.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,
|
||||
)
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
# Verify server capabilities
|
||||
assert "textDocumentSync" in init_response["capabilities"]
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert "definitionProvider" in init_response["capabilities"]
|
||||
|
||||
# Intelephense server is typically ready immediately after initialization
|
||||
self.server_ready.set()
|
||||
await self.server_ready.wait()
|
||||
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!
|
||||
|
||||
yield self
|
||||
|
||||
@override
|
||||
# For some reason, the LS may need longer to process this, so we just retry
|
||||
async def _send_references_request(self, relative_file_path: str, line: int, column: int):
|
||||
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 await super()._send_references_request(relative_file_path, line, column)
|
||||
return super()._send_references_request(relative_file_path, line, column)
|
||||
|
||||
@override
|
||||
async def _send_definition_request(self, definition_params: DefinitionParams):
|
||||
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 await super()._send_definition_request(definition_params)
|
||||
return super()._send_definition_request(definition_params)
|
||||
|
||||
@@ -2,25 +2,22 @@
|
||||
Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator, Tuple
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
from multilspy.language_server import LanguageServer
|
||||
from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
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(LanguageServer):
|
||||
class PyrightServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides Python specific instantiation of the LanguageServer class using Pyright.
|
||||
Contains various configurations and settings specific to Python.
|
||||
@@ -39,9 +36,9 @@ class PyrightServer(LanguageServer):
|
||||
ProcessLaunchInfo(cmd="python -m pyright.langserver --stdio", cwd=repository_root_path),
|
||||
"python",
|
||||
)
|
||||
|
||||
|
||||
# Event to signal when initial workspace analysis is complete
|
||||
self.analysis_complete = asyncio.Event()
|
||||
self.analysis_complete = threading.Event()
|
||||
self.found_source_files = False
|
||||
|
||||
@override
|
||||
@@ -148,11 +145,10 @@ class PyrightServer(LanguageServer):
|
||||
|
||||
return initialize_params
|
||||
|
||||
@asynccontextmanager
|
||||
async def start_server(self) -> AsyncIterator["PyrightServer"]:
|
||||
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.
|
||||
|
||||
@@ -167,20 +163,20 @@ class PyrightServer(LanguageServer):
|
||||
```
|
||||
"""
|
||||
|
||||
async def execute_client_command_handler(params):
|
||||
def execute_client_command_handler(params):
|
||||
return []
|
||||
|
||||
async def do_nothing(params):
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
async def window_log_message(msg):
|
||||
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):
|
||||
@@ -189,7 +185,7 @@ class PyrightServer(LanguageServer):
|
||||
self.analysis_complete.set()
|
||||
self.completions_available.set()
|
||||
|
||||
async def check_experimental_status(params):
|
||||
def check_experimental_status(params):
|
||||
"""
|
||||
Also listen for experimental/serverStatus as a backup signal
|
||||
"""
|
||||
@@ -209,38 +205,34 @@ class PyrightServer(LanguageServer):
|
||||
self.server.on_notification("language/actionableNotification", do_nothing)
|
||||
self.server.on_notification("experimental/serverStatus", check_experimental_status)
|
||||
|
||||
async with super().start_server():
|
||||
self.logger.log("Starting pyright-langserver server process", logging.INFO)
|
||||
await self.server.start()
|
||||
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)
|
||||
# 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 = await self.server.send.initialize(initialize_params)
|
||||
self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO)
|
||||
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"]
|
||||
# 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)
|
||||
try:
|
||||
await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0)
|
||||
self.logger.log("Pyright initial analysis complete, server ready", logging.INFO)
|
||||
except asyncio.TimeoutError:
|
||||
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()
|
||||
# Complete the initialization handshake
|
||||
self.server.notify.initialized({})
|
||||
|
||||
yield self
|
||||
# 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()
|
||||
|
||||
+1
-222
@@ -68,7 +68,7 @@ class SolidLanguageServer(ABC):
|
||||
PyrightServer,
|
||||
)
|
||||
|
||||
return SolidPyrightServer(config, logger, repository_root_path)
|
||||
return PyrightServer(config, logger, repository_root_path)
|
||||
# It used to be jedi, but pyright is a bit faster, and also more actively maintained
|
||||
# Keeping the previous code for reference
|
||||
# from multilspy.language_servers.jedi_language_server.jedi_server import (
|
||||
@@ -1633,224 +1633,3 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
def is_running(self) -> bool:
|
||||
return self.server.is_running()
|
||||
|
||||
|
||||
class SolidPyrightServer(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()
|
||||
|
||||
Reference in New Issue
Block a user