Refactor LSP manager architecture and event + completion pipeline

- Replace legacy LSPManagerClient + LSPManagerUI with ClientManager and UIManager
- Remove WebsocketClient in favor of unified Websocket implementation
- Move LSP initialization config loading into centralized config module
- Update LSPClient to support dynamic socket, improved init params, and doc version tracking
- Introduce range-based didChange notifications and add implementation + references requests
- Expand LSPClientEvents to support implementation/references and structured range edits
- Simplify websocket response handling and normalize LSP response parsing flow
- Decouple UI from LSP manager core; UI now emits address/port for client creation
- Refactor completion provider pipeline:
  - Split TextChangedEvent into TextInsertedEvent and DeleteRangeEvent
  - Update ProviderResponseCacheBase and controller dispatch paths accordingly
- Improve SourceBuffer and SourceFile event tracking with delete-range support
- Update plugin system:
  - Centralize command/provider registration via helper methods
  - Add LSP commands: definition, references, implementation, toggle UI
- Enhance response handlers to support references and implementation hooks
- Improve Python LSP config (jedi completion, signatures, references enabled)
- Fix minor GTK lifecycle and buffer signal handling issues
- Clean up unused imports, dead code, and outdated JSON server configs
This commit is contained in:
2026-04-11 15:36:59 -05:00
parent 0dc21cbb82
commit a8ad015e05
34 changed files with 896 additions and 575 deletions

View File

@@ -1,13 +1,9 @@
# Python imports
import threading
from os import path
import json
# Lib imports
import gi
from gi.repository import GLib
# Application imports
from ..config import get_lsp_init_config
from ..dto.code.lsp.lsp_messages import get_message_str
from ..dto.code.lsp.lsp_message_structs import \
LSPResponseTypes, ClientRequest, ClientNotification
@@ -19,29 +15,14 @@ class LSPClient(LSPClientWebsocket):
def __init__(self):
super(LSPClient, self).__init__()
self._socket: str = ""
self._language: str = ""
self._workspace_path: str = ""
self._init_params: dict = {}
self._init_opts: dict = {}
try:
_USER_HOME = path.expanduser('~')
_SCRIPT_PTH = path.dirname( path.realpath(__file__) )
_LSP_INIT_CONFIG = f"{_SCRIPT_PTH}/../configs/initialize-params-slim.json"
with open(_LSP_INIT_CONFIG) as file:
data = file.read()
self._init_params = json.loads(data)
except Exception as e:
logger.error( f"LSP Controller: {_LSP_INIT_CONFIG}\n\t\t{repr(e)}" )
self._socket = None
self._message_id: int = -1
self._event_history: dict[int, str] = {}
self.read_lock = threading.Lock()
self.write_lock = threading.Lock()
self._init_params: dict = get_lsp_init_config()
self._init_opts: dict[str, str] = {}
self.doc_vers: dict[str, int] = {}
def set_language(self, language: str):
@@ -57,7 +38,7 @@ class LSPClient(LSPClientWebsocket):
self._socket = socket
def unset_socket(self):
self._socket = None
self._socket = ""
def send_notification(self, method: str, params: dict = {}):
self._send_message( ClientNotification(method, params) )
@@ -71,5 +52,5 @@ class LSPClient(LSPClientWebsocket):
if not message_id in self._event_history: return
return self._event_history[message_id]
def handle_lsp_response(self, lsp_response: LSPResponseTypes):
def handle_lsp_response(self, lsp_response: LSPResponseTypes | dict):
raise NotImplementedError

View File

@@ -9,8 +9,10 @@ from ..dto.code.lsp.lsp_messages import didopen_notification
from ..dto.code.lsp.lsp_messages import didsave_notification
from ..dto.code.lsp.lsp_messages import didclose_notification
from ..dto.code.lsp.lsp_messages import didchange_notification
from ..dto.code.lsp.lsp_messages import didchange_notification_range
from ..dto.code.lsp.lsp_messages import completion_request
from ..dto.code.lsp.lsp_messages import definition_request
from ..dto.code.lsp.lsp_messages import implementation_request
from ..dto.code.lsp.lsp_messages import references_request
from ..dto.code.lsp.lsp_messages import symbols_request
@@ -40,6 +42,7 @@ class LSPClientEvents:
def _lsp_did_open(self, data: dict):
method = "textDocument/didOpen"
params = didopen_notification["params"]
self.doc_vers[ data["uri"] ] = -1
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
@@ -77,24 +80,24 @@ class LSPClientEvents:
self.send_notification( method, params )
# def _lsp_did_change(self, data: dict):
# method = "textDocument/didChange"
# params = didchange_notification_range["params"]
def _lsp_did_change_range(self, data: dict):
method = "textDocument/didChange"
params = didchange_notification_range["params"]
# params["textDocument"]["uri"] = data["uri"]
# params["textDocument"]["languageId"] = data["language_id"]
# params["textDocument"]["version"] = data["version"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
# contentChanges = params["contentChanges"][0]
# start = contentChanges["range"]["start"]
# end = contentChanges["range"]["end"]
# contentChanges["text"] = data["text"]
# start["line"] = data["line"]
# start["character"] = 0
# end["line"] = data["line"]
# end["character"] = data["column"]
contentChanges = params["contentChanges"][0]
start = contentChanges["range"]["start"]
end = contentChanges["range"]["end"]
contentChanges["text"] = data["text"]
start["line"] = data["line"]
start["character"] = data["column"]
end["line"] = data["end_line"]
end["character"] = data["end_column"]
# self.send_notification( method, params )
self.send_notification( method, params )
def _lsp_definition(self, data: dict):
method = "textDocument/definition"
@@ -108,13 +111,33 @@ class LSPClientEvents:
self.send_request( method, params )
def _lsp_implementation(self, data: dict):
method = "textDocument/implementation"
params = implementation_request["params"]
params["textDocument"]["uri"] = data["uri"]
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]
self.send_request( method, params )
def _lsp_references(self, data: dict):
method = "textDocument/references"
params = references_request["params"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]
self.send_request( method, params )
def _lsp_completion(self, data: dict):
method = "textDocument/completion"
params = completion_request["params"]
params["textDocument"]["uri"] = data["uri"]
params["textDocument"]["languageId"] = data["language_id"]
params["textDocument"]["version"] = data["version"]
params["position"]["line"] = data["line"]
params["position"]["character"] = data["column"]

View File

@@ -11,7 +11,7 @@ from ..dto.code.lsp.lsp_message_structs import \
LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
from .lsp_client_base import LSPClientBase
from .websocket_client import WebsocketClient
from .websocket import Websocket
@@ -24,26 +24,26 @@ class LSPClientWebsocket(LSPClientBase):
message = f"Content-Length: {message_size}\r\n\r\n{message_str}"
logger.debug(f"Client: {message_str}")
self.ws_client.send(message_str)
self.websocket.send(message_str)
def start_client(self):
self.ws_client = WebsocketClient()
self.ws_client.set_socket(self._socket)
self.ws_client.set_callback(self._monitor_lsp_response)
self.ws_client.start_client()
self.websocket = Websocket()
self.websocket.set_socket(self._socket)
self.websocket.set_callback(self._monitor_lsp_response)
self.websocket.start_client()
return self.ws_client
return self.websocket
def stop_client(self):
if not hasattr(self, "ws_client"): return
self.ws_client.close_client()
if not hasattr(self, "websocket"): return
self.websocket.close_client()
def _monitor_lsp_response(self, data: dict | None):
if not data: return
if not data: return {}
message = get_message_obj(data)
keys = message.keys()
lsp_response = None
lsp_response = data
if "result" in keys:
lsp_response = LSPResponseRequest(**get_message_obj(data))
@@ -51,6 +51,7 @@ class LSPClientWebsocket(LSPClientBase):
if "method" in keys:
lsp_response = LSPResponseNotification(**get_message_obj(data)) if not "id" in keys else LSPIDResponseNotification( **get_message_obj(data) )
if not lsp_response: return
if isinstance(lsp_response, str):
lsp_response = get_message_obj(lsp_response)
GLib.idle_add(self.handle_lsp_response, lsp_response)
GLib.idle_add(self.handle_lsp_response, lsp_response)

View File

@@ -9,7 +9,7 @@ from ..libs import websocket
class WebsocketClient:
class Websocket:
def __init__(self):
self.ws = None
self._socket = None
@@ -59,4 +59,4 @@ class WebsocketClient:
on_error = self.on_error,
on_close = self.on_close)
self.ws.run_forever(reconnect = 0.5)
self.ws.run_forever(reconnect = 0.5)