From 83ff7b8d7853df1cf17424763cd2475226e6b26f Mon Sep 17 00:00:00 2001 From: Hendrik Dumith Louzada Date: Mon, 8 Sep 2025 12:42:44 -0300 Subject: [PATCH 1/7] feat: add pygls initial implementaiton --- pylsp/server/__init__.py | 298 ++++++++++++++++++++++++++++++++++++++ pylsp/server/protocol.py | 129 +++++++++++++++++ pylsp/server/workspace.py | 84 +++++++++++ 3 files changed, 511 insertions(+) create mode 100644 pylsp/server/__init__.py create mode 100644 pylsp/server/protocol.py create mode 100644 pylsp/server/workspace.py diff --git a/pylsp/server/__init__.py b/pylsp/server/__init__.py new file mode 100644 index 00000000..71946732 --- /dev/null +++ b/pylsp/server/__init__.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import asyncio +import logging +import os +import typing as typ + +from lsprotocol import types as lsptyp +from pygls import server + +from pylsp import PYLSP, __version__ +from pylsp._utils import flatten, is_process_alive +from pylsp.server.protocol import LangageServerProtocol + +if typ.TYPE_CHECKING: + from pylsp.server.workspace import Workspace + +logger = logging.getLogger(__name__) + +MAX_WORKERS = 64 +PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s + +CONFIG_FILES = ("pycodestyle.cfg", "setup.cfg", "tox.ini", ".flake8") + + +class LanguageServer(server.LanguageServer): + """Python Language Server.""" + + lsp: LangageServerProtocol + + @property + def workspace(self) -> Workspace: + """Returns in-memory workspace.""" + return self.lsp.workspace + + def check_parent_process(self): + """Check if the parent process is still alive.""" + async def watch_parent_process(): + ppid = os.getppid() + while True: + if self._stop_event is not None and self._stop_event.is_set(): + break + if not is_process_alive(ppid): + self.shutdown() + break + await asyncio.sleep(PARENT_PROCESS_WATCH_INTERVAL) + asyncio.create_task(watch_parent_process()) + + +LSP_SERVER = LanguageServer( + name=PYLSP, + version=__version__, + max_workers=MAX_WORKERS, + protocol_cls=LangageServerProtocol, + text_document_sync_kind=lsptyp.TextDocumentSyncKind.Incremental, + notebook_document_sync=lsptyp.NotebookDocumentSyncOptions( + notebook_selector=[ + lsptyp.NotebookDocumentSyncOptionsNotebookSelectorType2( + cells=[ + lsptyp.NotebookDocumentSyncOptionsNotebookSelectorType2CellsType( + language="python" + ), + ], + ), + ], + ), +) + + +@LSP_SERVER.feature(lsptyp.INITIALIZE) +async def initialize(ls: LanguageServer, params: lsptyp.InitializeParams): + """Handle the initialization request.""" + # Call the initialization hook + await ls.lsp.call_hook("pylsp_initialize") + +@LSP_SERVER.feature(lsptyp.INITIALIZED) +async def initialized(ls: LanguageServer, params: lsptyp.InitializedParams): + """Handle the initialized notification.""" + # Call the initialized hook + await ls.lsp.call_hook("pylsp_initialized") + +@LSP_SERVER.feature(lsptyp.WORKSPACE_DID_CHANGE_CONFIGURATION) +async def workspace_did_change_configuration(ls: LanguageServer, params: lsptyp.WorkspaceConfigurationParams): + """Handle the workspace did change configuration notification.""" + for config_item in params.items: + ls.workspace.config.update({config_item.scope_uri: config_item.section}) + # TODO: Check configuration update is valid and supports this type of update + await ls.lsp.call_hook("pylsp_workspace_configuration_changed") + +@LSP_SERVER.feature(lsptyp.WORKSPACE_DID_CHANGE_WATCHED_FILES) +def workspace_did_change_watched_files(ls: LanguageServer, params: lsptyp.DidChangeWatchedFilesParams): + """Handle the workspace did change watched files notification.""" + for change in params.changes: + if change.uri.endswith(CONFIG_FILES): + ls.workspace.config.settings.cache_clear() + break + + # TODO: check if necessary to link files not handled by textDocument/Open + +@LSP_SERVER.feature(lsptyp.WORKSPACE_EXECUTE_COMMAND) +async def workspace_execute_command(ls: LanguageServer, params: lsptyp.ExecuteCommandParams): + """Handle the workspace execute command request.""" + # Call the execute command hook + await ls.lsp.call_hook("pylsp_execute_command", command=params.command, arguments=params.arguments, work_done_token=params.work_done_token) + +@LSP_SERVER.feature(lsptyp.NOTEBOOK_DOCUMENT_DID_OPEN) +async def notebook_document_did_open(ls: LanguageServer, params: lsptyp.DidOpenNotebookDocumentParams): + """Handle the notebook document did open notification.""" + await ls.lsp.lint_notebook_document(params.notebook_document.uri) + +@LSP_SERVER.feature(lsptyp.NOTEBOOK_DOCUMENT_DID_CHANGE) +async def notebook_document_did_change(ls: LanguageServer, params: lsptyp.DidChangeNotebookDocumentParams): + """Handle the notebook document did change notification.""" + await ls.lsp.lint_notebook_document(params.notebook_document.uri) + +@LSP_SERVER.feature(lsptyp.NOTEBOOK_DOCUMENT_DID_SAVE) +async def notebook_document_did_save(ls: LanguageServer, params: lsptyp.DidSaveNotebookDocumentParams): + """Handle the notebook document did save notification.""" + await ls.lsp.lint_notebook_document(params.notebook_document.uri) + await ls.workspace.save(params.notebook_document.uri) + +@LSP_SERVER.feature(lsptyp.NOTEBOOK_DOCUMENT_DID_CLOSE) +async def notebook_document_did_close(ls: LanguageServer, params: lsptyp.DidCloseNotebookDocumentParams): + """Handle the notebook document did close notification.""" + await ls.lsp.cancel_tasks(params.notebook_document.uri) + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DID_OPEN) +async def text_document_did_open(ls: LanguageServer, params: lsptyp.DidOpenTextDocumentParams): + """Handle the text document did open notification.""" + await ls.lsp.lint_text_document(params.text_document.uri) + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DID_CHANGE) +async def text_document_did_change(ls: LanguageServer, params: lsptyp.DidChangeTextDocumentParams): + """Handle the text document did change notification.""" + await ls.lsp.lint_text_document(params.text_document.uri) + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DID_SAVE) +async def text_document_did_save(ls: LanguageServer, params: lsptyp.DidSaveTextDocumentParams): + """Handle the text document did save notification.""" + await ls.lsp.lint_text_document(params.text_document.uri) + await ls.workspace.save(params.text_document.uri) + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DID_CLOSE) +async def text_document_did_close(ls: LanguageServer, params: lsptyp.DidCloseTextDocumentParams): + """Handle the text document did close notification.""" + await ls.lsp.cancel_tasks(params.text_document.uri) + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_CODE_ACTION) +async def text_document_code_action(ls: LanguageServer, params: lsptyp.CodeActionParams) -> typ.List[lsptyp.Command | lsptyp.CodeAction] | None: + """Handle the text document code action request.""" + actions: typ.List[lsptyp.Command | lsptyp.CodeAction] | None = flatten(await ls.lsp.call_hook( + "pylsp_code_action", + params.text_document.uri, + range=params.range, + context=params.context, + work_done_token=params.work_done_token, + )) + return actions + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_CODE_LENS) +async def text_document_code_lens(ls: LanguageServer, params: lsptyp.CodeLensParams) -> typ.List[lsptyp.CodeLens] | None: + """Handle the text document code lens request.""" + lenses: typ.List[lsptyp.CodeLens] | None = flatten(await ls.lsp.call_hook( + "pylsp_code_lens", + params.text_document.uri, + work_done_token=params.work_done_token, + )) + return lenses + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_COMPLETION) +async def text_document_completion(ls: LanguageServer, params: lsptyp.CompletionParams) -> typ.List[lsptyp.CompletionItem] | None: + """Handle the text document completion request.""" + completions: typ.List[lsptyp.CompletionItem] | None = flatten(await ls.lsp.call_hook( + "pylsp_completion", + params.text_document.uri, + position=params.position, + context=params.context, + work_done_token=params.work_done_token, + )) + return completions + +@LSP_SERVER.feature(lsptyp.COMPLETION_ITEM_RESOLVE) +async def completion_item_resolve(ls: LanguageServer, params: lsptyp.CompletionItem) -> lsptyp.CompletionItem | None: + """Handle the completion item resolve request.""" + item: lsptyp.CompletionItem | None = await ls.lsp.call_hook( + "pylsp_completion_item_resolve", + (params.data or {}).get("doc_uri"), + completion_item=params, + ) + return item + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DEFINITION) +async def text_document_definition(ls: LanguageServer, params: lsptyp.DefinitionParams) -> lsptyp.Location | None: + """Handle the text document definition request.""" + location: lsptyp.Location | None = await ls.lsp.call_hook( + "pylsp_definitions", + params.text_document.uri, + position=params.position, + work_done_token=params.work_done_token, + ) + return location + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_TYPE_DEFINITION) +async def text_document_type_definition(ls: LanguageServer, params: lsptyp.TypeDefinitionParams) -> lsptyp.Location | None: + """Handle the text document type definition request.""" + location: lsptyp.Location | None = await ls.lsp.call_hook( + "pylsp_type_definition", + params.text_document.uri, + position=params.position, + work_done_token=params.work_done_token, + ) + return location + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT) +async def text_document_document_highlight(ls: LanguageServer, params: lsptyp.DocumentHighlightParams) -> typ.List[lsptyp.DocumentHighlight] | None: + """Handle the text document document highlight request.""" + highlights: typ.List[lsptyp.DocumentHighlight] | None = flatten(await ls.lsp.call_hook( + "pylsp_document_highlight", + params.text_document.uri, + position=params.position, + work_done_token=params.work_done_token, + )) + return highlights + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_HOVER) +async def text_document_hover(ls: LanguageServer, params: lsptyp.HoverParams) -> lsptyp.Hover | None: + """Handle the text document hover request.""" + hover: lsptyp.Hover = await ls.lsp.call_hook( + "pylsp_hover", + params.text_document.uri, + position=params.position, + work_done_token=params.work_done_token, + ) + return hover + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) +async def text_document_document_symbol(ls: LanguageServer, params: lsptyp.DocumentSymbolParams) -> typ.List[lsptyp.DocumentSymbol] | None: + """Handle the text document document symbol request.""" + symbols: typ.List[lsptyp.DocumentSymbol] | None = flatten(await ls.lsp.call_hook( + "pylsp_document_symbols", + params.text_document.uri, + work_done_token=params.work_done_token, + )) + return symbols + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_FORMATTING) +async def text_document_formatting(ls: LanguageServer, params: lsptyp.DocumentFormattingParams) -> typ.List[lsptyp.TextEdit] | None: + """Handle the text document formatting request.""" + edits: typ.List[lsptyp.TextEdit] | None = flatten(await ls.lsp.call_hook( + "pylsp_format_document", + params.text_document.uri, + options=params.options, + )) + return edits + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_RANGE_FORMATTING) +async def text_document_range_formatting(ls: LanguageServer, params: lsptyp.DocumentRangeFormattingParams) -> typ.List[lsptyp.TextEdit] | None: + """Handle the text document range formatting request.""" + edits: typ.List[lsptyp.TextEdit] | None = flatten(await ls.lsp.call_hook( + "pylsp_format_range", + params.text_document.uri, + range=params.range, + options=params.options, + )) + return edits + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_FOLDING_RANGE) +async def text_document_folding_range(ls: LanguageServer, params: lsptyp.FoldingRangeParams) -> typ.List[lsptyp.FoldingRange] | None: + """Handle the text document folding range request.""" + ranges: typ.List[lsptyp.FoldingRange] | None = flatten(await ls.lsp.call_hook( + "pylsp_folding_range", + params.text_document.uri, + work_done_token=params.work_done_token, + )) + return ranges + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_REFERENCES) +async def text_document_references(ls: LanguageServer, params: lsptyp.ReferenceParams) -> typ.List[lsptyp.Location] | None: + """Handle the text document references request.""" + locations: typ.List[lsptyp.Location] | None = flatten(await ls.lsp.call_hook( + "pylsp_references", + params.text_document.uri, + position=params.position, + context=params.context, + )) + return locations + +@LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_SIGNATURE_HELP) +async def text_document_signature_help(ls: LanguageServer, params: lsptyp.SignatureHelpParams) -> lsptyp.SignatureHelp | None: + """Handle the text document signature help request.""" + signature_help: lsptyp.SignatureHelp | None = await ls.lsp.call_hook( + "pylsp_signature_help", + params.text_document.uri, + position=params.position, + work_done_token=params.work_done_token, + ) + return signature_help + diff --git a/pylsp/server/protocol.py b/pylsp/server/protocol.py new file mode 100644 index 00000000..39043e85 --- /dev/null +++ b/pylsp/server/protocol.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import json +import logging +import typing as typ +from functools import partial + +from lsprotocol import types as typlsp +from lsprotocol.types import ( + INITIALIZE, + InitializeParams, + InitializeResult, + TraceValues, +) +from pygls import protocol +from pygls.capabilities import ServerCapabilitiesBuilder +from pygls.uris import from_fs_path + +from pylsp import PYLSP, hookspecs +from pylsp.config.config import PluginManager +from pylsp.server.workspace import Workspace + +logger = logging.getLogger(__name__) + +class LangageServerProtocol(protocol.LanguageServerProtocol): + """Custom features implementation for the Python Language Server.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._pm = PluginManager(PYLSP) + if logger.level <= logging.DEBUG: + self._pm.trace.root.setwriter(logger.debug) + self._pm.enable_tracing() + self._pm.add_hookspecs(hookspecs) + + @property + def plugin_manager(self) -> PluginManager: + """Returns the plugin manager.""" + return self._pm + + @property + def workspace(self) -> Workspace: + if self._workspace is None: + raise RuntimeError( + "The workspace is not available - has the server been initialized?" + ) + + return typ.cast(Workspace, self._workspace) + + @protocol.lsp_method(INITIALIZE) + def lsp_initialize(self, params: InitializeParams) -> InitializeResult: + """Method that initializes language server. + It will compute and return server capabilities based on + registered features. + """ + logger.info("Language server initialized %s", params) + + self._server.process_id = params.process_id + self.initialization_options = params.initialization_options or {} + + text_document_sync_kind = self._server._text_document_sync_kind + notebook_document_sync = self._server._notebook_document_sync + + # Initialize server capabilities + self.client_capabilities = params.capabilities + self.server_capabilities = ServerCapabilitiesBuilder( + self.client_capabilities, + set({**self.fm.features, **self.fm.builtin_features}.keys()), + self.fm.feature_options, + list(self.fm.commands.keys()), + text_document_sync_kind, + notebook_document_sync, + ).build() + logger.debug( + "Server capabilities: %s", + json.dumps(self.server_capabilities, default=self._serialize_message), + ) + + root_path = params.root_path + root_uri = params.root_uri + if root_path is not None and root_uri is None: + root_uri = from_fs_path(root_path) + + # Initialize the workspace + workspace_folders = params.workspace_folders or [] + self._workspace = Workspace( + self._server, + root_uri, + text_document_sync_kind, + workspace_folders, + self.server_capabilities.position_encoding, + ) + + self.trace = TraceValues.Off + + return InitializeResult( + capabilities=self.server_capabilities, + server_info=self.server_info, + ) + + async def call_hook(self, hook_name: str, doc_uri: str | None = None, work_done_token: typlsp.ProgressToken | None = None, **kwargs): + """Calls hook_name and returns a list of results from all registered handlers. + + Args: + hook_name (str): The name of the hook to call. + doc_uri (str | None): The document URI to pass to the hook. + work_done_token (ProgressToken | None): The progress token to use for reporting progress. + **kwargs: Additional keyword arguments to pass to the hook. + """ + if doc_uri: + doc = self.workspace.get_text_document(doc_uri) + else: + doc = None + + workspace_folder = self.workspace.get_document_folder(doc_uri) if doc_uri else None + + folder_uri = workspace_folder.uri if workspace_folder else self.workspace._root_uri + + hook_handlers_caller = self.plugin_manager.subset_hook_caller( + hook_name, self.workspace.config.disabled_plugins + ) + + if work_done_token is not None: + await self.progress.create_async(work_done_token) + + return await self._server.loop.run_in_executor( + self._server.thread_pool_executor, + partial(hook_handlers_caller, lsp=self, workspace=folder_uri, document=doc, **kwargs), + ) diff --git a/pylsp/server/workspace.py b/pylsp/server/workspace.py new file mode 100644 index 00000000..16dd90ba --- /dev/null +++ b/pylsp/server/workspace.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +import typing as typ +from contextlib import suppress +from pathlib import Path + +from lsprotocol.types import WorkspaceFolder +from pygls import uris, workspace +from pygls.workspace.text_document import TextDocument + +from pylsp.config.config import Config + +if typ.TYPE_CHECKING: + from pygls.server import LanguageServer + + +class Workspace(workspace.Workspace): + """Custom Workspace class for pylsp.""" + def __init__(self, server: LanguageServer, *args, **kwargs): + self._server = server + super().__init__(*args, **kwargs) + self._config = Config( + self._root_uri, + self._server.lsp.initialization_options, + self._server.process_id, + self._server.server_capabilities + ) + + @property + def config(self) -> Config: + return self._config + + def get_document_folder(self, doc_uri: str) -> WorkspaceFolder | None: + """Get the workspace folder for a given document URI. + + Finds the folder that is the longest prefix of the document URI. + + Args: + doc_uri (str): The document URI. + + Returns: + WorkspaceFolder | None: The workspace folder containing the document, or None if not found. + """ + best_match_len = float('inf') + best_match = None + document_path = Path(uris.to_fs_path(doc_uri) or '') + for folder_uri, folder in self._folders.items(): + folder_path = Path(uris.to_fs_path(folder_uri) or '') + if match_len := len(document_path.relative_to(folder_path).parts) < best_match_len: + best_match_len = match_len + best_match = folder + + return best_match + + def _create_text_document( + self, + doc_uri: str, + source: str | None = None, + version: int | None = None, + language_id: str | None = None, + ) -> Document: + return Document( + self, + doc_uri, + source=source, + version=version, + language_id=language_id, + sync_kind=self._sync_kind, + position_codec=self._position_codec, + ) + + +class Document(TextDocument): + def __init__(self, workspace: Workspace, *args, **kwargs): + self._workspace = workspace + super().__init__(*args, **kwargs) + + @property + def workspace(self) -> Workspace: + return self._workspace + + @property + def workspace_folder(self) -> WorkspaceFolder | None: + return self._workspace.get_document_folder(self.uri) From 11628f20d12310cf1c63d192fa831815a27f8ec7 Mon Sep 17 00:00:00 2001 From: Hendrik Dumith Louzada Date: Mon, 8 Sep 2025 12:43:34 -0300 Subject: [PATCH 2/7] feat: add flatten utils func --- pylsp/_utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pylsp/_utils.py b/pylsp/_utils.py index dfe84b14..3dea0612 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -416,3 +416,9 @@ def get_eol_chars(text): if match: return match.group(0) return None + +def flatten(lst: Iterable[Iterable[Any]]) -> List[Any] | None: + """Flatten a iterable of iterables into a single list.""" + if not lst: + return None + return [i for sublst in lst for i in sublst] From f62e5b5cc0eaa05db85c66138dc132708db76634 Mon Sep 17 00:00:00 2001 From: Hendrik Dumith Louzada Date: Mon, 8 Sep 2025 12:44:17 -0300 Subject: [PATCH 3/7] feat: use pygls lsp server --- pylsp/__main__.py | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/pylsp/__main__.py b/pylsp/__main__.py index 760f8829..641b9cc3 100644 --- a/pylsp/__main__.py +++ b/pylsp/__main__.py @@ -4,7 +4,6 @@ import argparse import logging import logging.config -import sys import time try: @@ -12,13 +11,8 @@ except Exception: import json -from ._version import __version__ -from .python_lsp import ( - PythonLSPServer, - start_io_lang_server, - start_tcp_lang_server, - start_ws_lang_server, -) +from pylsp import __version__ +from pylsp.server import LSP_SERVER LOG_FORMAT = "%(asctime)s {} - %(levelname)s - %(name)s - %(message)s".format( time.localtime().tm_zone @@ -73,25 +67,19 @@ def main() -> None: args = parser.parse_args() _configure_logger(args.verbose, args.log_config, args.log_file) + if args.check_parent_process: + LSP_SERVER.check_parent_process() + if args.tcp: - start_tcp_lang_server( - args.host, args.port, args.check_parent_process, PythonLSPServer + LSP_SERVER.start_tcp( + args.host, args.port ) elif args.ws: - start_ws_lang_server(args.port, args.check_parent_process, PythonLSPServer) + LSP_SERVER.start_ws( + args.host, args.port, + ) else: - stdin, stdout = _binary_stdio() - start_io_lang_server(stdin, stdout, args.check_parent_process, PythonLSPServer) - - -def _binary_stdio(): - """Construct binary stdio streams (not text mode). - - This seems to be different for Window/Unix Python2/3, so going by: - https://stackoverflow.com/questions/2850893/reading-binary-data-from-stdin - """ - stdin, stdout = sys.stdin.buffer, sys.stdout.buffer - return stdin, stdout + LSP_SERVER.start_io() def _configure_logger(verbose=0, log_config=None, log_file=None) -> None: From 15fbbcb1417088c861a390ee5fc9d127a99425ea Mon Sep 17 00:00:00 2001 From: Hendrik Dumith Louzada Date: Mon, 8 Sep 2025 12:44:41 -0300 Subject: [PATCH 4/7] fix: add missing imports for utils --- pylsp/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp/_utils.py b/pylsp/_utils.py index 3dea0612..abfb5068 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -11,7 +11,7 @@ import sys import threading import time -from typing import Optional +from typing import Any, Iterable, List, Optional import docstring_to_markdown import jedi From b9056e7314eb8bcc7eeb9a2f1aec116fdcfbebe1 Mon Sep 17 00:00:00 2001 From: Hendrik Dumith Louzada Date: Wed, 17 Sep 2025 22:10:16 -0300 Subject: [PATCH 5/7] refac: apply ruff format --- pylsp/__main__.py | 7 +- pylsp/_utils.py | 1 + pylsp/server/__init__.py | 260 ++++++++++++++++++++++++++------------ pylsp/server/protocol.py | 25 +++- pylsp/server/workspace.py | 14 +- 5 files changed, 214 insertions(+), 93 deletions(-) diff --git a/pylsp/__main__.py b/pylsp/__main__.py index 641b9cc3..7871456a 100644 --- a/pylsp/__main__.py +++ b/pylsp/__main__.py @@ -71,12 +71,11 @@ def main() -> None: LSP_SERVER.check_parent_process() if args.tcp: - LSP_SERVER.start_tcp( - args.host, args.port - ) + LSP_SERVER.start_tcp(args.host, args.port) elif args.ws: LSP_SERVER.start_ws( - args.host, args.port, + args.host, + args.port, ) else: LSP_SERVER.start_io() diff --git a/pylsp/_utils.py b/pylsp/_utils.py index abfb5068..a29e382e 100644 --- a/pylsp/_utils.py +++ b/pylsp/_utils.py @@ -417,6 +417,7 @@ def get_eol_chars(text): return match.group(0) return None + def flatten(lst: Iterable[Iterable[Any]]) -> List[Any] | None: """Flatten a iterable of iterables into a single list.""" if not lst: diff --git a/pylsp/server/__init__.py b/pylsp/server/__init__.py index 71946732..37104f31 100644 --- a/pylsp/server/__init__.py +++ b/pylsp/server/__init__.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) MAX_WORKERS = 64 -PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s +PARENT_PROCESS_WATCH_INTERVAL = 10 # 10 s CONFIG_FILES = ("pycodestyle.cfg", "setup.cfg", "tox.ini", ".flake8") @@ -35,6 +35,7 @@ def workspace(self) -> Workspace: def check_parent_process(self): """Check if the parent process is still alive.""" + async def watch_parent_process(): ppid = os.getppid() while True: @@ -44,6 +45,7 @@ async def watch_parent_process(): self.shutdown() break await asyncio.sleep(PARENT_PROCESS_WATCH_INTERVAL) + asyncio.create_task(watch_parent_process()) @@ -73,22 +75,29 @@ async def initialize(ls: LanguageServer, params: lsptyp.InitializeParams): # Call the initialization hook await ls.lsp.call_hook("pylsp_initialize") + @LSP_SERVER.feature(lsptyp.INITIALIZED) async def initialized(ls: LanguageServer, params: lsptyp.InitializedParams): """Handle the initialized notification.""" # Call the initialized hook await ls.lsp.call_hook("pylsp_initialized") + @LSP_SERVER.feature(lsptyp.WORKSPACE_DID_CHANGE_CONFIGURATION) -async def workspace_did_change_configuration(ls: LanguageServer, params: lsptyp.WorkspaceConfigurationParams): +async def workspace_did_change_configuration( + ls: LanguageServer, params: lsptyp.WorkspaceConfigurationParams +): """Handle the workspace did change configuration notification.""" for config_item in params.items: ls.workspace.config.update({config_item.scope_uri: config_item.section}) # TODO: Check configuration update is valid and supports this type of update await ls.lsp.call_hook("pylsp_workspace_configuration_changed") + @LSP_SERVER.feature(lsptyp.WORKSPACE_DID_CHANGE_WATCHED_FILES) -def workspace_did_change_watched_files(ls: LanguageServer, params: lsptyp.DidChangeWatchedFilesParams): +def workspace_did_change_watched_files( + ls: LanguageServer, params: lsptyp.DidChangeWatchedFilesParams +): """Handle the workspace did change watched files notification.""" for change in params.changes: if change.uri.endswith(CONFIG_FILES): @@ -97,90 +106,140 @@ def workspace_did_change_watched_files(ls: LanguageServer, params: lsptyp.DidCha # TODO: check if necessary to link files not handled by textDocument/Open + @LSP_SERVER.feature(lsptyp.WORKSPACE_EXECUTE_COMMAND) -async def workspace_execute_command(ls: LanguageServer, params: lsptyp.ExecuteCommandParams): +async def workspace_execute_command( + ls: LanguageServer, params: lsptyp.ExecuteCommandParams +): """Handle the workspace execute command request.""" # Call the execute command hook - await ls.lsp.call_hook("pylsp_execute_command", command=params.command, arguments=params.arguments, work_done_token=params.work_done_token) + await ls.lsp.call_hook( + "pylsp_execute_command", + command=params.command, + arguments=params.arguments, + work_done_token=params.work_done_token, + ) + @LSP_SERVER.feature(lsptyp.NOTEBOOK_DOCUMENT_DID_OPEN) -async def notebook_document_did_open(ls: LanguageServer, params: lsptyp.DidOpenNotebookDocumentParams): +async def notebook_document_did_open( + ls: LanguageServer, params: lsptyp.DidOpenNotebookDocumentParams +): """Handle the notebook document did open notification.""" await ls.lsp.lint_notebook_document(params.notebook_document.uri) + @LSP_SERVER.feature(lsptyp.NOTEBOOK_DOCUMENT_DID_CHANGE) -async def notebook_document_did_change(ls: LanguageServer, params: lsptyp.DidChangeNotebookDocumentParams): +async def notebook_document_did_change( + ls: LanguageServer, params: lsptyp.DidChangeNotebookDocumentParams +): """Handle the notebook document did change notification.""" await ls.lsp.lint_notebook_document(params.notebook_document.uri) + @LSP_SERVER.feature(lsptyp.NOTEBOOK_DOCUMENT_DID_SAVE) -async def notebook_document_did_save(ls: LanguageServer, params: lsptyp.DidSaveNotebookDocumentParams): +async def notebook_document_did_save( + ls: LanguageServer, params: lsptyp.DidSaveNotebookDocumentParams +): """Handle the notebook document did save notification.""" await ls.lsp.lint_notebook_document(params.notebook_document.uri) await ls.workspace.save(params.notebook_document.uri) + @LSP_SERVER.feature(lsptyp.NOTEBOOK_DOCUMENT_DID_CLOSE) -async def notebook_document_did_close(ls: LanguageServer, params: lsptyp.DidCloseNotebookDocumentParams): +async def notebook_document_did_close( + ls: LanguageServer, params: lsptyp.DidCloseNotebookDocumentParams +): """Handle the notebook document did close notification.""" await ls.lsp.cancel_tasks(params.notebook_document.uri) + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DID_OPEN) -async def text_document_did_open(ls: LanguageServer, params: lsptyp.DidOpenTextDocumentParams): +async def text_document_did_open( + ls: LanguageServer, params: lsptyp.DidOpenTextDocumentParams +): """Handle the text document did open notification.""" await ls.lsp.lint_text_document(params.text_document.uri) + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DID_CHANGE) -async def text_document_did_change(ls: LanguageServer, params: lsptyp.DidChangeTextDocumentParams): +async def text_document_did_change( + ls: LanguageServer, params: lsptyp.DidChangeTextDocumentParams +): """Handle the text document did change notification.""" await ls.lsp.lint_text_document(params.text_document.uri) + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DID_SAVE) -async def text_document_did_save(ls: LanguageServer, params: lsptyp.DidSaveTextDocumentParams): +async def text_document_did_save( + ls: LanguageServer, params: lsptyp.DidSaveTextDocumentParams +): """Handle the text document did save notification.""" await ls.lsp.lint_text_document(params.text_document.uri) await ls.workspace.save(params.text_document.uri) + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DID_CLOSE) -async def text_document_did_close(ls: LanguageServer, params: lsptyp.DidCloseTextDocumentParams): +async def text_document_did_close( + ls: LanguageServer, params: lsptyp.DidCloseTextDocumentParams +): """Handle the text document did close notification.""" await ls.lsp.cancel_tasks(params.text_document.uri) + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_CODE_ACTION) -async def text_document_code_action(ls: LanguageServer, params: lsptyp.CodeActionParams) -> typ.List[lsptyp.Command | lsptyp.CodeAction] | None: +async def text_document_code_action( + ls: LanguageServer, params: lsptyp.CodeActionParams +) -> typ.List[lsptyp.Command | lsptyp.CodeAction] | None: """Handle the text document code action request.""" - actions: typ.List[lsptyp.Command | lsptyp.CodeAction] | None = flatten(await ls.lsp.call_hook( - "pylsp_code_action", - params.text_document.uri, - range=params.range, - context=params.context, - work_done_token=params.work_done_token, - )) + actions: typ.List[lsptyp.Command | lsptyp.CodeAction] | None = flatten( + await ls.lsp.call_hook( + "pylsp_code_action", + params.text_document.uri, + range=params.range, + context=params.context, + work_done_token=params.work_done_token, + ) + ) return actions + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_CODE_LENS) -async def text_document_code_lens(ls: LanguageServer, params: lsptyp.CodeLensParams) -> typ.List[lsptyp.CodeLens] | None: +async def text_document_code_lens( + ls: LanguageServer, params: lsptyp.CodeLensParams +) -> typ.List[lsptyp.CodeLens] | None: """Handle the text document code lens request.""" - lenses: typ.List[lsptyp.CodeLens] | None = flatten(await ls.lsp.call_hook( - "pylsp_code_lens", - params.text_document.uri, - work_done_token=params.work_done_token, - )) + lenses: typ.List[lsptyp.CodeLens] | None = flatten( + await ls.lsp.call_hook( + "pylsp_code_lens", + params.text_document.uri, + work_done_token=params.work_done_token, + ) + ) return lenses + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_COMPLETION) -async def text_document_completion(ls: LanguageServer, params: lsptyp.CompletionParams) -> typ.List[lsptyp.CompletionItem] | None: +async def text_document_completion( + ls: LanguageServer, params: lsptyp.CompletionParams +) -> typ.List[lsptyp.CompletionItem] | None: """Handle the text document completion request.""" - completions: typ.List[lsptyp.CompletionItem] | None = flatten(await ls.lsp.call_hook( - "pylsp_completion", - params.text_document.uri, - position=params.position, - context=params.context, - work_done_token=params.work_done_token, - )) + completions: typ.List[lsptyp.CompletionItem] | None = flatten( + await ls.lsp.call_hook( + "pylsp_completion", + params.text_document.uri, + position=params.position, + context=params.context, + work_done_token=params.work_done_token, + ) + ) return completions + @LSP_SERVER.feature(lsptyp.COMPLETION_ITEM_RESOLVE) -async def completion_item_resolve(ls: LanguageServer, params: lsptyp.CompletionItem) -> lsptyp.CompletionItem | None: +async def completion_item_resolve( + ls: LanguageServer, params: lsptyp.CompletionItem +) -> lsptyp.CompletionItem | None: """Handle the completion item resolve request.""" item: lsptyp.CompletionItem | None = await ls.lsp.call_hook( "pylsp_completion_item_resolve", @@ -189,8 +248,11 @@ async def completion_item_resolve(ls: LanguageServer, params: lsptyp.CompletionI ) return item + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DEFINITION) -async def text_document_definition(ls: LanguageServer, params: lsptyp.DefinitionParams) -> lsptyp.Location | None: +async def text_document_definition( + ls: LanguageServer, params: lsptyp.DefinitionParams +) -> lsptyp.Location | None: """Handle the text document definition request.""" location: lsptyp.Location | None = await ls.lsp.call_hook( "pylsp_definitions", @@ -200,8 +262,11 @@ async def text_document_definition(ls: LanguageServer, params: lsptyp.Definition ) return location + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_TYPE_DEFINITION) -async def text_document_type_definition(ls: LanguageServer, params: lsptyp.TypeDefinitionParams) -> lsptyp.Location | None: +async def text_document_type_definition( + ls: LanguageServer, params: lsptyp.TypeDefinitionParams +) -> lsptyp.Location | None: """Handle the text document type definition request.""" location: lsptyp.Location | None = await ls.lsp.call_hook( "pylsp_type_definition", @@ -211,19 +276,27 @@ async def text_document_type_definition(ls: LanguageServer, params: lsptyp.TypeD ) return location + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT) -async def text_document_document_highlight(ls: LanguageServer, params: lsptyp.DocumentHighlightParams) -> typ.List[lsptyp.DocumentHighlight] | None: +async def text_document_document_highlight( + ls: LanguageServer, params: lsptyp.DocumentHighlightParams +) -> typ.List[lsptyp.DocumentHighlight] | None: """Handle the text document document highlight request.""" - highlights: typ.List[lsptyp.DocumentHighlight] | None = flatten(await ls.lsp.call_hook( - "pylsp_document_highlight", - params.text_document.uri, - position=params.position, - work_done_token=params.work_done_token, - )) + highlights: typ.List[lsptyp.DocumentHighlight] | None = flatten( + await ls.lsp.call_hook( + "pylsp_document_highlight", + params.text_document.uri, + position=params.position, + work_done_token=params.work_done_token, + ) + ) return highlights + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_HOVER) -async def text_document_hover(ls: LanguageServer, params: lsptyp.HoverParams) -> lsptyp.Hover | None: +async def text_document_hover( + ls: LanguageServer, params: lsptyp.HoverParams +) -> lsptyp.Hover | None: """Handle the text document hover request.""" hover: lsptyp.Hover = await ls.lsp.call_hook( "pylsp_hover", @@ -233,60 +306,88 @@ async def text_document_hover(ls: LanguageServer, params: lsptyp.HoverParams) -> ) return hover + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_DOCUMENT_SYMBOL) -async def text_document_document_symbol(ls: LanguageServer, params: lsptyp.DocumentSymbolParams) -> typ.List[lsptyp.DocumentSymbol] | None: +async def text_document_document_symbol( + ls: LanguageServer, params: lsptyp.DocumentSymbolParams +) -> typ.List[lsptyp.DocumentSymbol] | None: """Handle the text document document symbol request.""" - symbols: typ.List[lsptyp.DocumentSymbol] | None = flatten(await ls.lsp.call_hook( - "pylsp_document_symbols", - params.text_document.uri, - work_done_token=params.work_done_token, - )) + symbols: typ.List[lsptyp.DocumentSymbol] | None = flatten( + await ls.lsp.call_hook( + "pylsp_document_symbols", + params.text_document.uri, + work_done_token=params.work_done_token, + ) + ) return symbols + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_FORMATTING) -async def text_document_formatting(ls: LanguageServer, params: lsptyp.DocumentFormattingParams) -> typ.List[lsptyp.TextEdit] | None: +async def text_document_formatting( + ls: LanguageServer, params: lsptyp.DocumentFormattingParams +) -> typ.List[lsptyp.TextEdit] | None: """Handle the text document formatting request.""" - edits: typ.List[lsptyp.TextEdit] | None = flatten(await ls.lsp.call_hook( - "pylsp_format_document", - params.text_document.uri, - options=params.options, - )) + edits: typ.List[lsptyp.TextEdit] | None = flatten( + await ls.lsp.call_hook( + "pylsp_format_document", + params.text_document.uri, + options=params.options, + ) + ) return edits + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_RANGE_FORMATTING) -async def text_document_range_formatting(ls: LanguageServer, params: lsptyp.DocumentRangeFormattingParams) -> typ.List[lsptyp.TextEdit] | None: +async def text_document_range_formatting( + ls: LanguageServer, params: lsptyp.DocumentRangeFormattingParams +) -> typ.List[lsptyp.TextEdit] | None: """Handle the text document range formatting request.""" - edits: typ.List[lsptyp.TextEdit] | None = flatten(await ls.lsp.call_hook( - "pylsp_format_range", - params.text_document.uri, - range=params.range, - options=params.options, - )) + edits: typ.List[lsptyp.TextEdit] | None = flatten( + await ls.lsp.call_hook( + "pylsp_format_range", + params.text_document.uri, + range=params.range, + options=params.options, + ) + ) return edits + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_FOLDING_RANGE) -async def text_document_folding_range(ls: LanguageServer, params: lsptyp.FoldingRangeParams) -> typ.List[lsptyp.FoldingRange] | None: +async def text_document_folding_range( + ls: LanguageServer, params: lsptyp.FoldingRangeParams +) -> typ.List[lsptyp.FoldingRange] | None: """Handle the text document folding range request.""" - ranges: typ.List[lsptyp.FoldingRange] | None = flatten(await ls.lsp.call_hook( - "pylsp_folding_range", - params.text_document.uri, - work_done_token=params.work_done_token, - )) + ranges: typ.List[lsptyp.FoldingRange] | None = flatten( + await ls.lsp.call_hook( + "pylsp_folding_range", + params.text_document.uri, + work_done_token=params.work_done_token, + ) + ) return ranges + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_REFERENCES) -async def text_document_references(ls: LanguageServer, params: lsptyp.ReferenceParams) -> typ.List[lsptyp.Location] | None: +async def text_document_references( + ls: LanguageServer, params: lsptyp.ReferenceParams +) -> typ.List[lsptyp.Location] | None: """Handle the text document references request.""" - locations: typ.List[lsptyp.Location] | None = flatten(await ls.lsp.call_hook( - "pylsp_references", - params.text_document.uri, - position=params.position, - context=params.context, - )) + locations: typ.List[lsptyp.Location] | None = flatten( + await ls.lsp.call_hook( + "pylsp_references", + params.text_document.uri, + position=params.position, + context=params.context, + ) + ) return locations + @LSP_SERVER.feature(lsptyp.TEXT_DOCUMENT_SIGNATURE_HELP) -async def text_document_signature_help(ls: LanguageServer, params: lsptyp.SignatureHelpParams) -> lsptyp.SignatureHelp | None: +async def text_document_signature_help( + ls: LanguageServer, params: lsptyp.SignatureHelpParams +) -> lsptyp.SignatureHelp | None: """Handle the text document signature help request.""" signature_help: lsptyp.SignatureHelp | None = await ls.lsp.call_hook( "pylsp_signature_help", @@ -295,4 +396,3 @@ async def text_document_signature_help(ls: LanguageServer, params: lsptyp.Signat work_done_token=params.work_done_token, ) return signature_help - diff --git a/pylsp/server/protocol.py b/pylsp/server/protocol.py index 39043e85..17427ff3 100644 --- a/pylsp/server/protocol.py +++ b/pylsp/server/protocol.py @@ -22,6 +22,7 @@ logger = logging.getLogger(__name__) + class LangageServerProtocol(protocol.LanguageServerProtocol): """Custom features implementation for the Python Language Server.""" @@ -98,7 +99,13 @@ def lsp_initialize(self, params: InitializeParams) -> InitializeResult: server_info=self.server_info, ) - async def call_hook(self, hook_name: str, doc_uri: str | None = None, work_done_token: typlsp.ProgressToken | None = None, **kwargs): + async def call_hook( + self, + hook_name: str, + doc_uri: str | None = None, + work_done_token: typlsp.ProgressToken | None = None, + **kwargs, + ): """Calls hook_name and returns a list of results from all registered handlers. Args: @@ -112,9 +119,13 @@ async def call_hook(self, hook_name: str, doc_uri: str | None = None, work_done_ else: doc = None - workspace_folder = self.workspace.get_document_folder(doc_uri) if doc_uri else None + workspace_folder = ( + self.workspace.get_document_folder(doc_uri) if doc_uri else None + ) - folder_uri = workspace_folder.uri if workspace_folder else self.workspace._root_uri + folder_uri = ( + workspace_folder.uri if workspace_folder else self.workspace._root_uri + ) hook_handlers_caller = self.plugin_manager.subset_hook_caller( hook_name, self.workspace.config.disabled_plugins @@ -125,5 +136,11 @@ async def call_hook(self, hook_name: str, doc_uri: str | None = None, work_done_ return await self._server.loop.run_in_executor( self._server.thread_pool_executor, - partial(hook_handlers_caller, lsp=self, workspace=folder_uri, document=doc, **kwargs), + partial( + hook_handlers_caller, + lsp=self, + workspace=folder_uri, + document=doc, + **kwargs, + ), ) diff --git a/pylsp/server/workspace.py b/pylsp/server/workspace.py index 16dd90ba..3144c61c 100644 --- a/pylsp/server/workspace.py +++ b/pylsp/server/workspace.py @@ -16,6 +16,7 @@ class Workspace(workspace.Workspace): """Custom Workspace class for pylsp.""" + def __init__(self, server: LanguageServer, *args, **kwargs): self._server = server super().__init__(*args, **kwargs) @@ -23,7 +24,7 @@ def __init__(self, server: LanguageServer, *args, **kwargs): self._root_uri, self._server.lsp.initialization_options, self._server.process_id, - self._server.server_capabilities + self._server.server_capabilities, ) @property @@ -41,12 +42,15 @@ def get_document_folder(self, doc_uri: str) -> WorkspaceFolder | None: Returns: WorkspaceFolder | None: The workspace folder containing the document, or None if not found. """ - best_match_len = float('inf') + best_match_len = float("inf") best_match = None - document_path = Path(uris.to_fs_path(doc_uri) or '') + document_path = Path(uris.to_fs_path(doc_uri) or "") for folder_uri, folder in self._folders.items(): - folder_path = Path(uris.to_fs_path(folder_uri) or '') - if match_len := len(document_path.relative_to(folder_path).parts) < best_match_len: + folder_path = Path(uris.to_fs_path(folder_uri) or "") + if ( + match_len := len(document_path.relative_to(folder_path).parts) + < best_match_len + ): best_match_len = match_len best_match = folder From ffa3101adcabf81b6b67b2c3239717c4a628089a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 17 Sep 2025 21:42:55 -0500 Subject: [PATCH 6/7] Remove unused import --- pylsp/server/workspace.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pylsp/server/workspace.py b/pylsp/server/workspace.py index 3144c61c..b30c7c28 100644 --- a/pylsp/server/workspace.py +++ b/pylsp/server/workspace.py @@ -1,7 +1,6 @@ from __future__ import annotations import typing as typ -from contextlib import suppress from pathlib import Path from lsprotocol.types import WorkspaceFolder From aef332ae49f90b7ce86d3b2b72fb5b6ba4ee60b0 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Wed, 17 Sep 2025 21:48:54 -0500 Subject: [PATCH 7/7] Break long sentences in docstrings --- pylsp/server/protocol.py | 3 ++- pylsp/server/workspace.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pylsp/server/protocol.py b/pylsp/server/protocol.py index 17427ff3..468e4d50 100644 --- a/pylsp/server/protocol.py +++ b/pylsp/server/protocol.py @@ -111,7 +111,8 @@ async def call_hook( Args: hook_name (str): The name of the hook to call. doc_uri (str | None): The document URI to pass to the hook. - work_done_token (ProgressToken | None): The progress token to use for reporting progress. + work_done_token (ProgressToken | None): The progress token to use for + reporting progress. **kwargs: Additional keyword arguments to pass to the hook. """ if doc_uri: diff --git a/pylsp/server/workspace.py b/pylsp/server/workspace.py index b30c7c28..9b988145 100644 --- a/pylsp/server/workspace.py +++ b/pylsp/server/workspace.py @@ -39,7 +39,8 @@ def get_document_folder(self, doc_uri: str) -> WorkspaceFolder | None: doc_uri (str): The document URI. Returns: - WorkspaceFolder | None: The workspace folder containing the document, or None if not found. + WorkspaceFolder | None: The workspace folder containing the document, or + None if not found. """ best_match_len = float("inf") best_match = None