refactor(lsp): restructure lsp plugin controller architecture and simplify provider cache
- Replace LSPManager usage with LSPController integration - Move UI access through lsp_controller.lsp_manager_ui - Remove legacy ProviderResponseCache client management - Simplify completion filtering and matcher handling - Improve typing annotations and modernize union syntax - Clean up unused imports and dead code - Fix completion item parsing for insertText/textEdit fallbacks - Add async-safe scrolling via GLib.idle_add
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# Python imports
|
||||
import threading
|
||||
from os import path
|
||||
import json
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
@@ -20,17 +22,15 @@ class LSPController(LSPControllerWebsocket):
|
||||
# initialize-params-slim.json was created off of jedi_language_server one
|
||||
# self._init_params = settings_manager.get_lsp_init_data()
|
||||
|
||||
self._language: str = ""
|
||||
self._init_params: dict = {}
|
||||
self._event_history: dict[str] = {}
|
||||
self._language: str = ""
|
||||
self._init_params: dict = {}
|
||||
self._event_history: dict[int, str] = {}
|
||||
|
||||
try:
|
||||
from os import path
|
||||
import json
|
||||
|
||||
_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().replace("{user.home}", _USER_HOME)
|
||||
self._init_params = json.loads(data)
|
||||
@@ -42,7 +42,7 @@ class LSPController(LSPControllerWebsocket):
|
||||
self.read_lock = threading.Lock()
|
||||
self.write_lock = threading.Lock()
|
||||
|
||||
def set_language(self, language):
|
||||
def set_language(self, language: str):
|
||||
self._language = language
|
||||
|
||||
def set_socket(self, socket: str):
|
||||
@@ -51,15 +51,15 @@ class LSPController(LSPControllerWebsocket):
|
||||
def unset_socket(self):
|
||||
self._socket = None
|
||||
|
||||
def send_notification(self, method: str, params: {} = {}):
|
||||
def send_notification(self, method: str, params: dict = {}):
|
||||
self._send_message( ClientNotification(method, params) )
|
||||
|
||||
def send_request(self, method: str, params: {} = {}):
|
||||
def send_request(self, method: str, params: dict = {}):
|
||||
self._message_id += 1
|
||||
self._event_history[self._message_id] = method
|
||||
self._send_message( ClientRequest(self._message_id, method, params) )
|
||||
|
||||
def get_event_by_id(self, message_id: int):
|
||||
def get_event_by_id(self, message_id: int) -> str:
|
||||
if not message_id in self._event_history: return
|
||||
return self._event_history[message_id]
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
# Python imports
|
||||
import traceback
|
||||
import subprocess
|
||||
|
||||
# Lib imports
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
# from libs import websockets
|
||||
from libs.dto.code.lsp.lsp_messages import LEN_HEADER, TYPE_HEADER, get_message_str, get_message_obj
|
||||
from libs.dto.code.lsp.lsp_message_structs import \
|
||||
LSPResponseTypes, ClientRequest, ClientNotification, LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
|
||||
from libs.dto.code.lsp.lsp_messages import get_message_str, get_message_obj
|
||||
from libs.dto.code.lsp.lsp_message_structs import \
|
||||
LSPResponseTypes, ClientRequest, ClientNotification, \
|
||||
LSPResponseRequest, LSPResponseNotification, LSPIDResponseNotification
|
||||
|
||||
from .lsp_controller_base import LSPControllerBase
|
||||
from .websocket_client import WebsocketClient
|
||||
@@ -17,7 +16,7 @@ from .websocket_client import WebsocketClient
|
||||
|
||||
|
||||
class LSPControllerWebsocket(LSPControllerBase):
|
||||
def _send_message(self, data: ClientRequest or ClientNotification):
|
||||
def _send_message(self, data: ClientRequest | ClientNotification):
|
||||
if not data: return
|
||||
|
||||
message_str = get_message_str(data)
|
||||
@@ -39,7 +38,7 @@ class LSPControllerWebsocket(LSPControllerBase):
|
||||
if not hasattr(self, "ws_client"): return
|
||||
self.ws_client.close_client()
|
||||
|
||||
def _monitor_lsp_response(self, data: None or {}):
|
||||
def _monitor_lsp_response(self, data: dict | None):
|
||||
if not data: return
|
||||
|
||||
message = get_message_obj(data)
|
||||
|
||||
5
plugins/code/ui/lsp_manager/handlers/__init__.py
Normal file
5
plugins/code/ui/lsp_manager/handlers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .base import BaseHandler
|
||||
from .default import DefaultHandler
|
||||
from .python import PythonHandler
|
||||
from .java import JavaHandler
|
||||
from .registry import HandlerRegistry
|
||||
31
plugins/code/ui/lsp_manager/handlers/base.py
Normal file
31
plugins/code/ui/lsp_manager/handlers/base.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Python imports
|
||||
from abc import ABC
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class BaseHandler:
|
||||
def __init__(self):
|
||||
self.context = None
|
||||
self.response_cache = None
|
||||
|
||||
|
||||
def set_context(self, context):
|
||||
self.context = context
|
||||
|
||||
def set_response_cache(self, response_cache):
|
||||
self.response_cache = response_cache
|
||||
|
||||
@property
|
||||
def emit(self):
|
||||
return self.context.emit
|
||||
|
||||
@property
|
||||
def emit_to(self):
|
||||
return self.context.emit_to
|
||||
|
||||
def handle(self, method: str, response, controller):
|
||||
pass
|
||||
100
plugins/code/ui/lsp_manager/handlers/default.py
Normal file
100
plugins/code/ui/lsp_manager/handlers/default.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||
|
||||
from .base import BaseHandler
|
||||
|
||||
|
||||
|
||||
class DefaultHandler(BaseHandler):
|
||||
"""Fallback handler for unknown languages - uses generic LSP handling."""
|
||||
|
||||
def handle(self, method: str, response, controller):
|
||||
match method:
|
||||
case "textDocument/completion":
|
||||
self._handle_completion(response)
|
||||
case "textDocument/definition":
|
||||
self._handle_definition(response, controller)
|
||||
case "textDocument/publishDiagnostics":
|
||||
self._handle_diagnostics(response)
|
||||
|
||||
def _handle_completion(self, result):
|
||||
if not result: return
|
||||
|
||||
items = result.get("items", []) if isinstance(result, dict) else result
|
||||
|
||||
self.response_cache.matchers.clear()
|
||||
for item in items:
|
||||
label = item.get("label")
|
||||
if not label:
|
||||
continue
|
||||
|
||||
text = (
|
||||
item.get("insertText")
|
||||
or item.get("textEdit", {}).get("newText")
|
||||
or item.get("textEditText", "")
|
||||
or label
|
||||
)
|
||||
|
||||
detail = item.get("detail")
|
||||
doc = item.get("documentation")
|
||||
|
||||
if detail:
|
||||
info = detail
|
||||
elif isinstance(doc, dict):
|
||||
info = doc.get("value", "")
|
||||
else:
|
||||
info = str(doc) if doc else ""
|
||||
|
||||
self.response_cache.matchers[label] = {
|
||||
"label": label,
|
||||
"text": text,
|
||||
"info": info,
|
||||
}
|
||||
|
||||
self.context._prompt_completion_request()
|
||||
|
||||
def _handle_definition(self, response, controller):
|
||||
if not response: return
|
||||
|
||||
uri = response[0]["uri"]
|
||||
self.context._prompt_goto_request(uri, response[0]["range"])
|
||||
|
||||
def _handle_diagnostics(self, params):
|
||||
if not params: return
|
||||
|
||||
uri = params.get("uri", "")
|
||||
diagnostics = params.get("diagnostics", [])
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
hints = []
|
||||
|
||||
for diag in diagnostics:
|
||||
severity = diag.get("severity", 1)
|
||||
message = diag.get("message", "")
|
||||
range = diag.get("range", {})
|
||||
|
||||
diag_info = {
|
||||
"message": message,
|
||||
"range": range
|
||||
}
|
||||
|
||||
if severity == 1:
|
||||
errors.append(diag_info)
|
||||
elif severity == 2:
|
||||
warnings.append(diag_info)
|
||||
elif severity == 3:
|
||||
hints.append(diag_info)
|
||||
|
||||
self.response_cache.lsp_diagnostics = {
|
||||
"uri": uri,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"hints": hints
|
||||
}
|
||||
|
||||
logger.debug(f"LSP Diagnostics for {uri}: {len(errors)} errors, {len(warnings)} warnings, {len(hints)} hints")
|
||||
51
plugins/code/ui/lsp_manager/handlers/java.py
Normal file
51
plugins/code/ui/lsp_manager/handlers/java.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('GtkSource', '4')
|
||||
|
||||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||
|
||||
from .default import DefaultHandler
|
||||
|
||||
|
||||
|
||||
class JavaHandler(DefaultHandler):
|
||||
"""Java-specific: overrides definition, handles classFileContents."""
|
||||
|
||||
def handle(self, method: str, response, controller):
|
||||
match method:
|
||||
case "textDocument/definition":
|
||||
self._handle_definition(response, controller)
|
||||
case "java/classFileContents":
|
||||
self._handle_class_file_contents(response)
|
||||
case _:
|
||||
super().handle(method, response, controller)
|
||||
|
||||
def _handle_definition(self, response, controller):
|
||||
if not response: return
|
||||
|
||||
uri = response[0]["uri"]
|
||||
if "jdt://" in uri:
|
||||
controller._lsp_java_class_file_contents(uri)
|
||||
return
|
||||
|
||||
self.context._prompt_goto_request(uri, response[0]["range"])
|
||||
|
||||
def _handle_class_file_contents(self, text: str):
|
||||
event = Event_Factory.create_event("get_active_view")
|
||||
self.context.emit_to("source_views", event)
|
||||
|
||||
view = event.response
|
||||
file = view.command.exec("new_file")
|
||||
buffer = view.get_buffer()
|
||||
itr = buffer.get_iter_at_mark(buffer.get_insert())
|
||||
lm = GtkSource.LanguageManager.get_default()
|
||||
language = lm.get_language("java")
|
||||
file.ftype = "java"
|
||||
|
||||
buffer.set_language(language)
|
||||
buffer.insert(itr, text, -1)
|
||||
12
plugins/code/ui/lsp_manager/handlers/python.py
Normal file
12
plugins/code/ui/lsp_manager/handlers/python.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .default import DefaultHandler
|
||||
|
||||
|
||||
|
||||
class PythonHandler(DefaultHandler):
|
||||
"""Uses default handling, can override if Python needs special logic."""
|
||||
...
|
||||
49
plugins/code/ui/lsp_manager/handlers/registry.py
Normal file
49
plugins/code/ui/lsp_manager/handlers/registry.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from ..mixins.lsp_server_events_mixin import LSPServerEventsMixin
|
||||
|
||||
from .base import BaseHandler
|
||||
from .default import DefaultHandler
|
||||
from .python import PythonHandler
|
||||
from .java import JavaHandler
|
||||
|
||||
|
||||
|
||||
class HandlerRegistry(LSPServerEventsMixin):
|
||||
def __init__(self):
|
||||
|
||||
self._instances: dict = {}
|
||||
self._lang_handlers: dict = {
|
||||
"default": DefaultHandler,
|
||||
"python": PythonHandler,
|
||||
"java": JavaHandler,
|
||||
}
|
||||
|
||||
|
||||
def _get_instance(self, handler_cls: type[BaseHandler]) -> BaseHandler:
|
||||
if handler_cls in self._instances: return self._instances[handler_cls]
|
||||
|
||||
self._instances[handler_cls] = handler_cls()
|
||||
|
||||
return self._instances[handler_cls]
|
||||
|
||||
def register_handler(self, lang_id: str, handler_cls: type[BaseHandler]):
|
||||
self._lang_handlers[lang_id] = handler_cls
|
||||
|
||||
def get_handler(self, lang_id: str = "", method: str = ""):
|
||||
handler_cls = self._lang_handlers.get(
|
||||
lang_id, self._lang_handlers.get("default", DefaultHandler)
|
||||
)
|
||||
|
||||
if not handler_cls: return None
|
||||
|
||||
return self._get_instance(handler_cls)
|
||||
|
||||
def close_handler(self, lang_id: str):
|
||||
if not lang_id in self._lang_handlers: return
|
||||
|
||||
handler_cls = self._lang_handlers[lang_id]
|
||||
self._instances.pop(handler_cls, None)
|
||||
57
plugins/code/ui/lsp_manager/lsp_client_controller.py
Normal file
57
plugins/code/ui/lsp_manager/lsp_client_controller.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Python imports
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .mixins.lsp_client_events_mixin import LSPClientEventsMixin
|
||||
from .controllers.lsp_controller import LSPController
|
||||
|
||||
|
||||
|
||||
class LSPClientController(LSPClientEventsMixin):
|
||||
def __init__(self):
|
||||
super(LSPClientController, self).__init__()
|
||||
|
||||
self._cache_refresh_timeout_id: int = None
|
||||
|
||||
self.executor: ThreadPoolExecutor = ThreadPoolExecutor(max_workers = 1)
|
||||
self.active_language_id: str = ""
|
||||
self.clients: dict = {}
|
||||
|
||||
|
||||
def create_client(
|
||||
self,
|
||||
lang_id: str = "python",
|
||||
workspace_uri: str = "",
|
||||
init_opts: dict = {}
|
||||
) -> LSPController:
|
||||
if lang_id in self.clients: return None
|
||||
|
||||
address = "127.0.0.1"
|
||||
port = 9999
|
||||
uri = f"ws://{address}:{port}/{lang_id}"
|
||||
client = LSPController()
|
||||
|
||||
client.set_language(lang_id)
|
||||
client.set_socket(uri)
|
||||
client.start_client()
|
||||
|
||||
if not client.ws_client.wait_for_connection(timeout = 5.0):
|
||||
logger.error(f"Failed to connect to LSP server for {lang_id}")
|
||||
return None
|
||||
|
||||
self.clients[lang_id] = client
|
||||
|
||||
return client
|
||||
|
||||
def close_client(self, lang_id: str) -> bool:
|
||||
if lang_id not in self.clients: return False
|
||||
|
||||
controller = self.clients.pop(lang_id)
|
||||
controller.stop_client()
|
||||
|
||||
return True
|
||||
|
||||
def get_active_client(self) -> LSPController:
|
||||
return self.clients[self.active_language_id]
|
||||
100
plugins/code/ui/lsp_manager/lsp_controller.py
Normal file
100
plugins/code/ui/lsp_manager/lsp_controller.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from libs.dto.code.lsp.lsp_message_structs import LSPResponseTypes, LSPResponseRequest, LSPResponseNotification
|
||||
|
||||
from .provider import Provider
|
||||
from .provider_response_cache import ProviderResponseCache
|
||||
from .lsp_manager_ui import LSPManagerUI
|
||||
from .lsp_client_controller import LSPClientController
|
||||
from .handlers.registry import HandlerRegistry
|
||||
|
||||
|
||||
|
||||
class LSPController:
|
||||
def __init__(self):
|
||||
super(LSPController, self).__init__()
|
||||
|
||||
self._init()
|
||||
self._load_widgets()
|
||||
self._do_bind_mapping()
|
||||
|
||||
|
||||
def _init(self):
|
||||
self.provider: Provider = Provider()
|
||||
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
||||
self.lsp_client_controller: LSPClientController = LSPClientController()
|
||||
self.handler_registry: HandlerRegistry = HandlerRegistry()
|
||||
|
||||
def _load_widgets(self):
|
||||
self.lsp_manager_ui: LSPManagerUI = LSPManagerUI()
|
||||
self.lsp_manager_ui.create_client = self.create_client
|
||||
self.lsp_manager_ui.close_client = self.close_client
|
||||
|
||||
def _do_bind_mapping(self):
|
||||
self.response_cache.process_file_load = self.lsp_client_controller.process_file_load
|
||||
self.response_cache.process_file_close = self.lsp_client_controller.process_file_close
|
||||
self.response_cache.process_file_save = self.lsp_client_controller.process_file_save
|
||||
self.response_cache.process_file_change = self.lsp_client_controller.process_file_change
|
||||
self.provider.response_cache = self.response_cache
|
||||
|
||||
|
||||
|
||||
def create_client(
|
||||
self,
|
||||
lang_id: str = "python",
|
||||
workspace_uri: str = "",
|
||||
init_opts: dict = {}
|
||||
) -> bool:
|
||||
client = self.lsp_client_controller.create_client(
|
||||
lang_id, workspace_uri, init_opts
|
||||
)
|
||||
handler = self.handler_registry.get_handler(lang_id)
|
||||
self.lsp_client_controller.active_language_id = lang_id
|
||||
|
||||
if not client or not handler:
|
||||
logger.error(f"LSP Controller: Either 'client' or 'handler' didn't get created...'")
|
||||
self.close_client(lang_id)
|
||||
return False
|
||||
|
||||
handler.set_context(self.handler_registry)
|
||||
handler.set_response_cache(self.response_cache)
|
||||
|
||||
client.handle_lsp_response = self.server_response
|
||||
client.send_initialize_message(init_opts, "", f"file://{workspace_uri}")
|
||||
|
||||
return True
|
||||
|
||||
def close_client(self, lang_id: str) -> bool:
|
||||
self.lsp_client_controller.close_client(lang_id)
|
||||
self.handler_registry.close_handler(lang_id)
|
||||
|
||||
return True
|
||||
|
||||
def server_response(self, lsp_response: LSPResponseTypes):
|
||||
logger.debug(f"LSP Response: { lsp_response }")
|
||||
|
||||
if isinstance(lsp_response, LSPResponseRequest):
|
||||
if not self.lsp_client_controller.active_language_id in self.lsp_client_controller.clients:
|
||||
logger.debug(f"No LSP client for '{self.lsp_client_controller.active_language_id}', skipping 'server_response'")
|
||||
return
|
||||
|
||||
controller = self.lsp_client_controller.get_active_client()
|
||||
event = controller.get_event_by_id(lsp_response.id)
|
||||
handler = self.handler_registry.get_handler(
|
||||
self.lsp_client_controller.active_language_id, event
|
||||
)
|
||||
|
||||
if not handler: return
|
||||
handler.handle(event, lsp_response.result, controller)
|
||||
elif isinstance(lsp_response, LSPResponseNotification):
|
||||
handler = self.handler_registry.get_handler("default", lsp_response.method)
|
||||
|
||||
if not handler: return
|
||||
|
||||
# TODO: Need to make default singleton so as to not need to set these here
|
||||
handler.set_context(self.handler_registry)
|
||||
handler.set_response_cache(self.response_cache)
|
||||
handler.handle(lsp_response.method, lsp_response.params, None)
|
||||
@@ -12,20 +12,18 @@ from gi.repository import GLib
|
||||
from gi.repository import GtkSource
|
||||
|
||||
# Application imports
|
||||
from .provider import Provider
|
||||
|
||||
|
||||
|
||||
class LSPManager(Gtk.Dialog):
|
||||
class LSPManagerUI(Gtk.Dialog):
|
||||
def __init__(self):
|
||||
super(LSPManager, self).__init__()
|
||||
super(LSPManagerUI, self).__init__()
|
||||
|
||||
self._SCRIPT_PTH: str = path.dirname( path.realpath(__file__) )
|
||||
self._USER_HOME: str = path.expanduser('~')
|
||||
self._LSP_SERVERS_CONFIG: str = ""
|
||||
self.servers_config: dict = {}
|
||||
|
||||
self.provider: Provider = Provider()
|
||||
self.parent = None
|
||||
self.source_view = None
|
||||
|
||||
@@ -69,8 +67,8 @@ class LSPManager(Gtk.Dialog):
|
||||
self.path_bttn.connect("file-set", self._file_set)
|
||||
self.path_bttn.set_halign(Gtk.Align.FILL)
|
||||
self.hide_bttn.connect("clicked", lambda widget: self.hide())
|
||||
create_client_bttn.connect("clicked", self.create_client, close_client_bttn)
|
||||
close_client_bttn.connect("clicked", self.close_client, create_client_bttn)
|
||||
create_client_bttn.connect("clicked", self._create_client, close_client_bttn)
|
||||
close_client_bttn.connect("clicked", self._close_client, create_client_bttn)
|
||||
|
||||
self.main_box.set_column_spacing(15)
|
||||
self.main_box.set_row_spacing(15)
|
||||
@@ -110,8 +108,7 @@ class LSPManager(Gtk.Dialog):
|
||||
widget.move(x, y)
|
||||
|
||||
def _path_changed(self, widget, buttons_widget):
|
||||
fpath = widget.get_text()
|
||||
if not fpath:
|
||||
if not widget.get_text():
|
||||
buttons_widget.hide()
|
||||
return
|
||||
|
||||
@@ -145,10 +142,16 @@ class LSPManager(Gtk.Dialog):
|
||||
scrolled_win.show_all()
|
||||
|
||||
def load_lsp_servers_config(self):
|
||||
with open(f"{self._SCRIPT_PTH}/configs/lsp-servers-config.json") as file:
|
||||
self._LSP_SERVERS_CONFIG = file.read()
|
||||
try:
|
||||
with open(f"{self._SCRIPT_PTH}/configs/lsp-servers-config.json") as file:
|
||||
self._LSP_SERVERS_CONFIG = file.read()
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Config file not found: {self._SCRIPT_PTH}/configs/lsp-servers-config.json")
|
||||
|
||||
def load_lsp_servers_config_placeholders(self):
|
||||
if not self._LSP_SERVERS_CONFIG: return
|
||||
if not self.source_view: return
|
||||
|
||||
data = self._LSP_SERVERS_CONFIG \
|
||||
.replace("{user.home}", self._USER_HOME) \
|
||||
.replace("{workspace.folder}", self.path_entry.get_text())
|
||||
@@ -162,24 +165,36 @@ class LSPManager(Gtk.Dialog):
|
||||
buffer.delete(start_itr, end_itr)
|
||||
buffer.insert(start_itr, data, -1)
|
||||
|
||||
self.set_language_combo_box( self.servers_config.keys() )
|
||||
self.set_language_combo_box( list(self.servers_config.keys()) )
|
||||
|
||||
def set_language_combo_box(self, lang_ids: list[str]):
|
||||
self.combo_box.remove_all()
|
||||
|
||||
for lang_id in lang_ids:
|
||||
self.combo_box.append_text(lang_id)
|
||||
|
||||
def create_client(self, widget, sibling):
|
||||
def _create_client(self, widget, sibling):
|
||||
if not self.source_view: return
|
||||
|
||||
buffer = self.source_view.get_buffer()
|
||||
lang_id = self.combo_box.get_active_text()
|
||||
|
||||
if not lang_id: return
|
||||
if not lang_id in self.servers_config: return
|
||||
|
||||
self.servers_config = json.loads( buffer.get_text( *buffer.get_bounds() ) )
|
||||
init_opts = self.servers_config[lang_id]["initialization-options"]
|
||||
workspace_dir = self.path_entry.get_text()
|
||||
try:
|
||||
self.servers_config = json.loads(
|
||||
buffer.get_text( *buffer.get_bounds() )
|
||||
)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Invalid JSON: {e}")
|
||||
return
|
||||
|
||||
result = self.provider.response_cache.create_client(
|
||||
init_opts = self.servers_config[lang_id]["initialization-options"]
|
||||
workspace_dir = self.path_entry.get_text()
|
||||
result = None
|
||||
|
||||
result = self.create_client(
|
||||
lang_id, workspace_dir, init_opts
|
||||
)
|
||||
|
||||
@@ -188,11 +203,11 @@ class LSPManager(Gtk.Dialog):
|
||||
widget.hide()
|
||||
sibling.show()
|
||||
|
||||
def close_client(self, widget, sibling):
|
||||
def _close_client(self, widget, sibling):
|
||||
lang_id = self.combo_box.get_active_text()
|
||||
|
||||
if not lang_id: return
|
||||
result = self.provider.response_cache.close_client(lang_id)
|
||||
result = self.close_client(lang_id)
|
||||
if not result: return
|
||||
|
||||
widget.hide()
|
||||
@@ -23,7 +23,7 @@ class LSPClientEventsMixin:
|
||||
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
|
||||
buffer = event.file.buffer
|
||||
text = buffer.get_text(*buffer.get_bounds())
|
||||
self._last_active_language_id = lang_id
|
||||
self.active_language_id = lang_id
|
||||
|
||||
controller._lsp_did_open({
|
||||
"uri": uri,
|
||||
@@ -54,7 +54,7 @@ class LSPClientEventsMixin:
|
||||
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
|
||||
buffer = event.file.buffer
|
||||
text = buffer.get_text(*buffer.get_bounds())
|
||||
self._last_active_language_id = lang_id
|
||||
self.active_language_id = lang_id
|
||||
|
||||
controller._lsp_did_save({"uri": uri, "text": text})
|
||||
|
||||
@@ -71,7 +71,7 @@ class LSPClientEventsMixin:
|
||||
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
|
||||
buffer = event.file.buffer
|
||||
text = buffer.get_text(*buffer.get_bounds())
|
||||
self._last_active_language_id = lang_id
|
||||
self.active_language_id = lang_id
|
||||
|
||||
controller._lsp_did_change({
|
||||
"uri": uri,
|
||||
@@ -97,7 +97,7 @@ class LSPClientEventsMixin:
|
||||
|
||||
controller = self.clients[lang_id]
|
||||
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
|
||||
self._last_active_language_id = lang_id
|
||||
self.active_language_id = lang_id
|
||||
|
||||
controller._lsp_definition({
|
||||
"uri": uri,
|
||||
@@ -116,7 +116,7 @@ class LSPClientEventsMixin:
|
||||
|
||||
controller = self.clients[lang_id]
|
||||
uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath
|
||||
self._last_active_language_id = lang_id
|
||||
self.active_language_id = lang_id
|
||||
|
||||
controller._lsp_completion({
|
||||
"uri": uri,
|
||||
|
||||
@@ -29,22 +29,25 @@ class LSPServerEventsMixin:
|
||||
|
||||
self.matchers.clear()
|
||||
for item in items:
|
||||
label = item.get("label", "")
|
||||
if not label: continue
|
||||
label = item.get("label")
|
||||
if not label: return None
|
||||
|
||||
text = item.get("insertText")
|
||||
if not text and "textEdit" in item:
|
||||
text = item["textEdit"].get("newText", "")
|
||||
text = (
|
||||
item.get("insertText")
|
||||
or item.get("textEdit", {}).get("newText")
|
||||
or item.get("textEditText", "")
|
||||
or label
|
||||
)
|
||||
|
||||
info = ""
|
||||
if "detail" in item:
|
||||
info = item["detail"]
|
||||
elif "documentation" in item:
|
||||
doc = item["documentation"]
|
||||
if isinstance(doc, dict):
|
||||
info = doc.get("value", "")
|
||||
else:
|
||||
info = str(doc)
|
||||
detail = item.get("detail")
|
||||
doc = item.get("documentation")
|
||||
|
||||
if detail:
|
||||
info = detail
|
||||
elif isinstance(doc, dict):
|
||||
info = doc.get("value", "")
|
||||
else:
|
||||
info = str(doc) if doc else ""
|
||||
|
||||
self.matchers[label] = {
|
||||
"label": label,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from libs.event_factory import Event_Factory, Code_Event_Types
|
||||
@@ -8,11 +11,11 @@ from libs.dto.states import SourceViewStates
|
||||
|
||||
from plugins.plugin_types import PluginCode
|
||||
|
||||
from .lsp_manager import LSPManager
|
||||
from .lsp_controller import LSPController
|
||||
|
||||
|
||||
|
||||
lsp_manager = LSPManager()
|
||||
lsp_controller = LSPController()
|
||||
|
||||
|
||||
|
||||
@@ -27,7 +30,7 @@ class Plugin(PluginCode):
|
||||
def load(self):
|
||||
window = self.request_ui_element("main-window")
|
||||
|
||||
lsp_manager.map_parent_resize_event(window)
|
||||
lsp_controller.lsp_manager_ui.map_parent_resize_event(window)
|
||||
|
||||
event = Event_Factory.create_event("register_command",
|
||||
command_name = "LSP Manager",
|
||||
@@ -37,10 +40,10 @@ class Plugin(PluginCode):
|
||||
)
|
||||
self.emit_to("source_views", event)
|
||||
|
||||
event = Event_Factory.create_event(
|
||||
event = Event_Factory.create_event(
|
||||
"register_provider",
|
||||
provider_name = "LSP Completer",
|
||||
provider = lsp_manager.provider,
|
||||
provider = lsp_controller.provider,
|
||||
language_ids = []
|
||||
)
|
||||
self.emit_to("completion", event)
|
||||
@@ -52,12 +55,14 @@ class Plugin(PluginCode):
|
||||
self.emit_to("source_views", event)
|
||||
|
||||
source_view = event.response
|
||||
lsp_manager.load_lsp_servers_config()
|
||||
lsp_manager.set_source_view(source_view)
|
||||
lsp_manager.load_lsp_servers_config_placeholders()
|
||||
lsp_manager.provider.response_cache.emit = self.emit
|
||||
lsp_manager.provider.response_cache.emit_to = self.emit_to
|
||||
lsp_manager.provider.response_cache._prompt_completion_request = self._prompt_completion_request
|
||||
lsp_controller.lsp_manager_ui.load_lsp_servers_config()
|
||||
lsp_controller.lsp_manager_ui.set_source_view(source_view)
|
||||
lsp_controller.lsp_manager_ui.load_lsp_servers_config_placeholders()
|
||||
|
||||
lsp_controller.handler_registry.emit = self.emit
|
||||
lsp_controller.handler_registry.emit_to = self.emit_to
|
||||
lsp_controller.handler_registry._prompt_goto_request = self._prompt_goto_request
|
||||
lsp_controller.handler_registry._prompt_completion_request = self._prompt_completion_request
|
||||
|
||||
def run(self):
|
||||
...
|
||||
@@ -65,6 +70,24 @@ class Plugin(PluginCode):
|
||||
def generate_plugin_element(self):
|
||||
...
|
||||
|
||||
def _prompt_goto_request(self, uri: str, pointer_pos: dict):
|
||||
event = Event_Factory.create_event(
|
||||
"get_active_view",
|
||||
)
|
||||
self.emit_to("source_views", event)
|
||||
view = event.response
|
||||
view._on_uri_data_received( [uri] )
|
||||
|
||||
buffer = view.get_buffer()
|
||||
|
||||
def move_cursor(buffer, pointer_pos):
|
||||
itr = buffer.get_iter_at_line( pointer_pos["end"]["line"] )
|
||||
itr.forward_chars( pointer_pos["end"]["character"] )
|
||||
buffer.place_cursor(itr)
|
||||
view.scroll_to_iter(itr, 0.2, False, 0, 0)
|
||||
|
||||
GLib.idle_add( move_cursor, buffer, pointer_pos )
|
||||
|
||||
def _prompt_completion_request(self):
|
||||
event = Event_Factory.create_event(
|
||||
"get_active_view",
|
||||
@@ -75,7 +98,7 @@ class Plugin(PluginCode):
|
||||
event = Event_Factory.create_event(
|
||||
"request_completion",
|
||||
view = view,
|
||||
provider = lsp_manager.provider
|
||||
provider = lsp_controller.provider
|
||||
)
|
||||
self.emit_to("completion", event)
|
||||
|
||||
@@ -98,7 +121,7 @@ class Handler:
|
||||
column = iter.get_line_offset()
|
||||
|
||||
if char_str == "g":
|
||||
lsp_manager.provider.response_cache.process_goto_definition(
|
||||
lsp_controller.lsp_client_controller.process_goto_definition(
|
||||
file.ftype, file.fpath, line, column
|
||||
)
|
||||
|
||||
@@ -107,4 +130,4 @@ class Handler:
|
||||
if char_str == "i":
|
||||
return
|
||||
|
||||
lsp_manager.hide() if lsp_manager.is_visible() else lsp_manager.show()
|
||||
lsp_controller.lsp_manager_ui.hide() if lsp_controller.lsp_manager_ui.is_visible() else lsp_controller.lsp_manager_ui.show()
|
||||
|
||||
@@ -22,7 +22,7 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
||||
def __init__(self):
|
||||
super(Provider, self).__init__()
|
||||
|
||||
self.response_cache: ProviderResponseCache = ProviderResponseCache()
|
||||
self.response_cache: ProviderResponseCache = None
|
||||
|
||||
|
||||
def pre_populate(self, context):
|
||||
@@ -32,13 +32,19 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider):
|
||||
return "LSP Code Completion"
|
||||
|
||||
def do_match(self, context):
|
||||
# Note: If provider is in interactive activation then need to check
|
||||
# view focus as otherwise non focus views start trying to grab it.
|
||||
# completion = context.get_property("completion")
|
||||
# if not completion.get_view().has_focus(): return
|
||||
|
||||
iter = self.response_cache.get_iter_correctly(context)
|
||||
iter.backward_char()
|
||||
ch = iter.get_char()
|
||||
|
||||
# NOTE: Look to re-add or apply supprting logic to use spaces
|
||||
# NOTE: Look to re-add or apply supporting logic to use spaces
|
||||
# As is it slows down the editor in certain contexts...
|
||||
if not (ch in ('_', '.', ' ') or ch.isalnum()):
|
||||
# if not (ch in ('_', '.', ' ') or ch.isalnum()):
|
||||
if not (ch in ('_', '.') or ch.isalnum()):
|
||||
return False
|
||||
|
||||
buffer = iter.get_buffer()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# Python imports
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import asyncio
|
||||
from asyncio import Queue
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
@@ -14,113 +12,17 @@ from libs.dto.code.lsp.lsp_message_structs import LSPResponseTypes, LSPResponseR
|
||||
|
||||
from core.widgets.code.completion_providers.provider_response_cache_base import ProviderResponseCacheBase
|
||||
|
||||
from .controllers.lsp_controller import LSPController
|
||||
from .mixins.lsp_client_events_mixin import LSPClientEventsMixin
|
||||
from .mixins.lsp_server_events_mixin import LSPServerEventsMixin
|
||||
|
||||
|
||||
|
||||
class ProviderResponseCache(LSPClientEventsMixin, LSPServerEventsMixin, ProviderResponseCacheBase):
|
||||
class ProviderResponseCache(ProviderResponseCacheBase):
|
||||
def __init__(self):
|
||||
super(ProviderResponseCache, self).__init__()
|
||||
|
||||
self.executor = ThreadPoolExecutor(max_workers = 1)
|
||||
self.matchers: dict = {}
|
||||
self.clients: dict = {}
|
||||
self._cache_refresh_timeout_id: int = None
|
||||
self._last_active_language_id: str = None
|
||||
|
||||
|
||||
def create_client(
|
||||
self,
|
||||
lang_id: str = "python",
|
||||
workspace_uri: str = "",
|
||||
init_opts: dict = {
|
||||
}) -> bool:
|
||||
if lang_id in self.clients: return False
|
||||
|
||||
address = "127.0.0.1"
|
||||
port = 9999
|
||||
uri = f"ws://{address}:{port}/{lang_id}"
|
||||
controller = LSPController()
|
||||
controller.handle_lsp_response = self.server_response
|
||||
|
||||
controller.set_language(lang_id)
|
||||
controller.set_socket(uri)
|
||||
controller.start_client()
|
||||
|
||||
if not controller.ws_client.wait_for_connection(timeout = 5.0):
|
||||
logger.error(f"Failed to connect to LSP server for {lang_id}")
|
||||
return False
|
||||
|
||||
self.clients[lang_id] = controller
|
||||
controller.send_initialize_message(init_opts, "", f"file://{workspace_uri}")
|
||||
|
||||
return True
|
||||
|
||||
def close_client(self, lang_id: str) -> bool:
|
||||
if lang_id not in self.clients: return False
|
||||
|
||||
controller = self.clients.pop(lang_id)
|
||||
controller.stop_client()
|
||||
|
||||
return True
|
||||
|
||||
# TODO: Need to map 'lang_id' to a given language response class and
|
||||
# pass the controller to a 'server_response' method there.
|
||||
# It would allow clean separation of each language's idiosyncracies
|
||||
def server_response(self, lsp_response: LSPResponseTypes):
|
||||
logger.debug(f"LSP Response: { lsp_response }")
|
||||
|
||||
if isinstance(lsp_response, LSPResponseRequest):
|
||||
if not self._last_active_language_id in self.clients:
|
||||
logger.debug(f"No LSP client for '{self._last_active_language_id}', skipping 'server_response'")
|
||||
return
|
||||
|
||||
controller = self.clients[self._last_active_language_id]
|
||||
event = controller.get_event_by_id(lsp_response.id)
|
||||
|
||||
match event:
|
||||
case "textDocument/completion":
|
||||
self._handle_completion_response(lsp_response.result)
|
||||
case "textDocument/definition":
|
||||
result = lsp_response.result
|
||||
if not result: return
|
||||
uri = result[0]["uri"]
|
||||
if "jdt://" in uri:
|
||||
controller._lsp_java_class_file_contents(uri)
|
||||
return
|
||||
|
||||
self._handle_definition_response(uri, result[0]["range"])
|
||||
case "java/classFileContents":
|
||||
self._handle_java_class_file_contents(lsp_response.result)
|
||||
case _:
|
||||
...
|
||||
elif isinstance(lsp_response, LSPResponseNotification):
|
||||
match lsp_response.method:
|
||||
case "textDocument/publishDiagnostics":
|
||||
...
|
||||
case _:
|
||||
...
|
||||
|
||||
def filter(self, word: str) -> list[dict]:
|
||||
return []
|
||||
|
||||
def filter_with_context(self, context: GtkSource.CompletionContext) -> list[dict]:
|
||||
response = []
|
||||
iter = self.get_iter_correctly(context)
|
||||
iter.backward_char()
|
||||
char_str = iter.get_char()
|
||||
|
||||
if char_str == "." or char_str == " ":
|
||||
for label, item in self.matchers.items():
|
||||
response.append(item)
|
||||
|
||||
return response
|
||||
|
||||
word = self.get_word(context).rstrip()
|
||||
for label, item in self.matchers.items():
|
||||
if label.startswith(word):
|
||||
response.append(item)
|
||||
|
||||
return response
|
||||
return list( self.matchers.values() )
|
||||
|
||||
8
plugins/code/ui/lsp_manager/scripts/CONFIG.sh
Normal file
8
plugins/code/ui/lsp_manager/scripts/CONFIG.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# set -o xtrace ## To debug scripts
|
||||
# set -o errexit ## To exit on error
|
||||
# set -o errunset ## To exit if a variable is referenced but not set
|
||||
|
||||
|
||||
CONTAINER="newton-lsp"
|
||||
38
plugins/code/ui/lsp_manager/scripts/start.sh
Executable file
38
plugins/code/ui/lsp_manager/scripts/start.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
. CONFIG.sh
|
||||
|
||||
# set -o xtrace ## To debug scripts
|
||||
# set -o errexit ## To exit on error
|
||||
# set -o errunset ## To exit if a variable is referenced but not set
|
||||
|
||||
|
||||
function main() {
|
||||
SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
cd "${SCRIPTPATH}"
|
||||
echo "Working Dir: " $(pwd)
|
||||
|
||||
ID=$(podman ps --filter "ancestor=localhost/${CONTAINER}:latest" --format "{{.ID}}")
|
||||
if [ "${ID}" != "" ]; then
|
||||
echo "Is up..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CODE_HOST="${HOME}/Coding"
|
||||
CODE_CONTAINER="${HOME}/Coding"
|
||||
CONFIG_HOST="${HOME}/.config/lsps"
|
||||
CONFIG_CONTAINER="${HOME}/.config/lsps"
|
||||
|
||||
# podman run -d -m 4G \
|
||||
podman run -m 4G \
|
||||
-p 9999:9999 \
|
||||
-e HOME="${HOME}" \
|
||||
-e MAVEN_OPTS="-Duser.home=${HOME}" \
|
||||
-e JAVA_TOOL_OPTIONS="-Duser.home=${HOME}" \
|
||||
-e JDTLS_CONFIG_PATH="${CONFIG_CONTAINER}/jdtls" \
|
||||
-e JDTLS_DATA_PATH="${JDTLS_CONFIG_PATH}/data" \
|
||||
-v "${CODE_HOST}":"${CODE_CONTAINER}" \
|
||||
-v "${CONFIG_HOST}":"${CONFIG_CONTAINER}" \
|
||||
"${CONTAINER}:latest"
|
||||
}
|
||||
main $@;
|
||||
23
plugins/code/ui/lsp_manager/scripts/stop.sh
Executable file
23
plugins/code/ui/lsp_manager/scripts/stop.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
|
||||
. CONFIG.sh
|
||||
|
||||
# set -o xtrace ## To debug scripts
|
||||
# set -o errexit ## To exit on error
|
||||
# set -o errunset ## To exit if a variable is referenced but not set
|
||||
|
||||
|
||||
function main() {
|
||||
SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
cd "${SCRIPTPATH}"
|
||||
echo "Working Dir: " $(pwd)
|
||||
|
||||
ID=$(podman ps --filter "ancestor=localhost/${CONTAINER}:latest" --format "{{.ID}}")
|
||||
if [ "${ID}" == "" ]; then
|
||||
echo "Is not up..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
podman container stop "${ID}"
|
||||
}
|
||||
main $@;
|
||||
Reference in New Issue
Block a user