diff --git a/src/solidlsp/language_servers/intelephense/intelephense.py b/src/solidlsp/language_servers/intelephense/intelephense.py index d2ad911..9b6eb95 100644 --- a/src/solidlsp/language_servers/intelephense/intelephense.py +++ b/src/solidlsp/language_servers/intelephense/intelephense.py @@ -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) diff --git a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py index 4885583..b349050 100644 --- a/src/solidlsp/language_servers/pyright_language_server/pyright_server.py +++ b/src/solidlsp/language_servers/pyright_language_server/pyright_server.py @@ -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 \ No newline at end of file + # Wait for Pyright to complete its initial workspace analysis + # This prevents zombie processes by ensuring background tasks finish + self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO) + if self.analysis_complete.wait(timeout=5.0): + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + else: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index bd1eca2..bafd33c 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -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()