Source code for simvx.ide.lsp.client

"""IDE-specific LSP client — thin wrapper around ``simvx.core.lsp.LSPClient``.

Routes LSP results through ``State`` signals and converts diagnostics to
the IDE's ``Diagnostic`` dataclass.
"""

import logging
from typing import Any

from simvx.core.lsp import LSPClient as _CoreLSPClient
from simvx.core.lsp.protocol import Diagnostic as ProtoDiagnostic
from simvx.core.lsp.protocol import uri_to_path

from ..state import Diagnostic as StateDiagnostic
from ..state import State

log = logging.getLogger(__name__)

[docs] class LSPClient(_CoreLSPClient): """LSP client that integrates with :class:`State`.""" def __init__( self, state: State, command: str = "pylsp", args: list[str] | None = None, env: dict[str, str] | None = None ): self.state = state super().__init__(command=command, args=args, env=env, root_path=state.project_root) # -- Override result handlers to route through State ---------------- def _handle_completion(self, result: Any): from simvx.core.ui.completion_types import CompletionItem if result is None: self.state.completion_received.emit([]) return raw_items = result.get("items", result) if isinstance(result, dict) else result if not isinstance(raw_items, list): self.state.completion_received.emit([]) return items = [CompletionItem.from_dict(item) for item in raw_items] items.sort(key=lambda c: c.sort_text or c.label) self.state.completion_received.emit(items) def _handle_definition(self, result: Any): from simvx.core.lsp.protocol import Location if result is None: self.state.definition_received.emit([]) return if isinstance(result, dict): result = [result] locations = [Location.from_dict(loc) for loc in result] self.state.definition_received.emit(locations) def _handle_hover(self, result: Any): from simvx.core.lsp.protocol import Hover if result is None: return hover = Hover.from_dict(result) line = col = 0 if hover.range: line = hover.range.start.line col = hover.range.start.character self.state.hover_received.emit(hover.contents, line, col) def _handle_references(self, result: Any): from simvx.core.lsp.protocol import Location if result is None: self.state.references_received.emit([]) return locations = [Location.from_dict(loc) for loc in result] self.state.references_received.emit(locations) def _handle_rename(self, result: Any): if result is None: return file_edits: dict[str, list[tuple[int, int, int, int, str]]] = {} changes = result.get("changes", {}) document_changes = result.get("documentChanges", []) if document_changes: for doc_change in document_changes: raw_edits = doc_change.get("edits", []) uri = doc_change.get("textDocument", {}).get("uri", "") path = uri_to_path(uri) file_edits[path] = self._convert_lsp_edits(raw_edits) elif changes: for uri, raw_edits in changes.items(): path = uri_to_path(uri) file_edits[path] = self._convert_lsp_edits(raw_edits) total = sum(len(e) for e in file_edits.values()) if file_edits: self.state.rename_edits_received.emit(file_edits) self.state.status_message.emit(f"Rename: {total} edits across {len(file_edits)} file(s)") else: self.state.status_message.emit("Rename: no edits returned") def _handle_formatting(self, result: Any, path: str): if not result: self.state.status_message.emit("Formatting: no edits returned") return edits = self._convert_lsp_edits(result) if edits: self.state.formatting_edits_received.emit(path, edits) self.state.status_message.emit(f"Formatting: {len(edits)} edits applied") else: self.state.status_message.emit("Formatting: no edits returned") def _handle_diagnostics(self, params: dict): uri = params.get("uri", "") path = uri_to_path(uri) raw_diags = params.get("diagnostics", []) diags = [] for d in raw_diags: proto_diag = ProtoDiagnostic.from_dict(d) diags.append( StateDiagnostic( path=path, line=proto_diag.range.start.line, col_start=proto_diag.range.start.character, col_end=proto_diag.range.end.character, severity=proto_diag.severity, message=proto_diag.message, source=proto_diag.source, code=proto_diag.code, ) ) self.state.set_diagnostics(path, diags)