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)