diff --git a/plugins/code/ui/lsp_manager/controllers/lsp_controller.py b/plugins/code/ui/lsp_manager/controllers/lsp_controller.py index 5ca022d..a5be028 100644 --- a/plugins/code/ui/lsp_manager/controllers/lsp_controller.py +++ b/plugins/code/ui/lsp_manager/controllers/lsp_controller.py @@ -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] diff --git a/plugins/code/ui/lsp_manager/controllers/lsp_controller_websocket.py b/plugins/code/ui/lsp_manager/controllers/lsp_controller_websocket.py index ed57896..4e2f256 100644 --- a/plugins/code/ui/lsp_manager/controllers/lsp_controller_websocket.py +++ b/plugins/code/ui/lsp_manager/controllers/lsp_controller_websocket.py @@ -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) diff --git a/plugins/code/ui/lsp_manager/handlers/__init__.py b/plugins/code/ui/lsp_manager/handlers/__init__.py new file mode 100644 index 0000000..30dffdc --- /dev/null +++ b/plugins/code/ui/lsp_manager/handlers/__init__.py @@ -0,0 +1,5 @@ +from .base import BaseHandler +from .default import DefaultHandler +from .python import PythonHandler +from .java import JavaHandler +from .registry import HandlerRegistry diff --git a/plugins/code/ui/lsp_manager/handlers/base.py b/plugins/code/ui/lsp_manager/handlers/base.py new file mode 100644 index 0000000..03c1edb --- /dev/null +++ b/plugins/code/ui/lsp_manager/handlers/base.py @@ -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 diff --git a/plugins/code/ui/lsp_manager/handlers/default.py b/plugins/code/ui/lsp_manager/handlers/default.py new file mode 100644 index 0000000..cfd9108 --- /dev/null +++ b/plugins/code/ui/lsp_manager/handlers/default.py @@ -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") diff --git a/plugins/code/ui/lsp_manager/handlers/java.py b/plugins/code/ui/lsp_manager/handlers/java.py new file mode 100644 index 0000000..b3d4de5 --- /dev/null +++ b/plugins/code/ui/lsp_manager/handlers/java.py @@ -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) diff --git a/plugins/code/ui/lsp_manager/handlers/python.py b/plugins/code/ui/lsp_manager/handlers/python.py new file mode 100644 index 0000000..776b1af --- /dev/null +++ b/plugins/code/ui/lsp_manager/handlers/python.py @@ -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.""" + ... diff --git a/plugins/code/ui/lsp_manager/handlers/registry.py b/plugins/code/ui/lsp_manager/handlers/registry.py new file mode 100644 index 0000000..938a275 --- /dev/null +++ b/plugins/code/ui/lsp_manager/handlers/registry.py @@ -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) diff --git a/plugins/code/ui/lsp_manager/lsp_client_controller.py b/plugins/code/ui/lsp_manager/lsp_client_controller.py new file mode 100644 index 0000000..87ed201 --- /dev/null +++ b/plugins/code/ui/lsp_manager/lsp_client_controller.py @@ -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] diff --git a/plugins/code/ui/lsp_manager/lsp_controller.py b/plugins/code/ui/lsp_manager/lsp_controller.py new file mode 100644 index 0000000..a854563 --- /dev/null +++ b/plugins/code/ui/lsp_manager/lsp_controller.py @@ -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) diff --git a/plugins/code/ui/lsp_manager/lsp_manager.py b/plugins/code/ui/lsp_manager/lsp_manager_ui.py similarity index 80% rename from plugins/code/ui/lsp_manager/lsp_manager.py rename to plugins/code/ui/lsp_manager/lsp_manager_ui.py index 8899be6..ac23002 100644 --- a/plugins/code/ui/lsp_manager/lsp_manager.py +++ b/plugins/code/ui/lsp_manager/lsp_manager_ui.py @@ -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() diff --git a/plugins/code/ui/lsp_manager/mixins/lsp_client_events_mixin.py b/plugins/code/ui/lsp_manager/mixins/lsp_client_events_mixin.py index 5cfdbf8..2344716 100644 --- a/plugins/code/ui/lsp_manager/mixins/lsp_client_events_mixin.py +++ b/plugins/code/ui/lsp_manager/mixins/lsp_client_events_mixin.py @@ -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, diff --git a/plugins/code/ui/lsp_manager/mixins/lsp_server_events_mixin.py b/plugins/code/ui/lsp_manager/mixins/lsp_server_events_mixin.py index a779893..6de5be9 100644 --- a/plugins/code/ui/lsp_manager/mixins/lsp_server_events_mixin.py +++ b/plugins/code/ui/lsp_manager/mixins/lsp_server_events_mixin.py @@ -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, diff --git a/plugins/code/ui/lsp_manager/plugin.py b/plugins/code/ui/lsp_manager/plugin.py index 00d1735..2dd2d1d 100644 --- a/plugins/code/ui/lsp_manager/plugin.py +++ b/plugins/code/ui/lsp_manager/plugin.py @@ -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() diff --git a/plugins/code/ui/lsp_manager/provider.py b/plugins/code/ui/lsp_manager/provider.py index 0efa2d0..a41e0e6 100644 --- a/plugins/code/ui/lsp_manager/provider.py +++ b/plugins/code/ui/lsp_manager/provider.py @@ -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() diff --git a/plugins/code/ui/lsp_manager/provider_response_cache.py b/plugins/code/ui/lsp_manager/provider_response_cache.py index bf51b22..7758f80 100644 --- a/plugins/code/ui/lsp_manager/provider_response_cache.py +++ b/plugins/code/ui/lsp_manager/provider_response_cache.py @@ -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() ) diff --git a/plugins/code/ui/lsp_manager/scripts/CONFIG.sh b/plugins/code/ui/lsp_manager/scripts/CONFIG.sh new file mode 100644 index 0000000..9ccc18c --- /dev/null +++ b/plugins/code/ui/lsp_manager/scripts/CONFIG.sh @@ -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" \ No newline at end of file diff --git a/plugins/code/ui/lsp_manager/scripts/start.sh b/plugins/code/ui/lsp_manager/scripts/start.sh new file mode 100755 index 0000000..c980ca0 --- /dev/null +++ b/plugins/code/ui/lsp_manager/scripts/start.sh @@ -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 $@; diff --git a/plugins/code/ui/lsp_manager/scripts/stop.sh b/plugins/code/ui/lsp_manager/scripts/stop.sh new file mode 100755 index 0000000..6f50bd4 --- /dev/null +++ b/plugins/code/ui/lsp_manager/scripts/stop.sh @@ -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 $@; diff --git a/src/core/widgets/code/command_system/commands/dnd_load_file_to_buffer.py b/src/core/widgets/code/command_system/commands/dnd_load_file_to_buffer.py index 4c7b791..2a27b92 100644 --- a/src/core/widgets/code/command_system/commands/dnd_load_file_to_buffer.py +++ b/src/core/widgets/code/command_system/commands/dnd_load_file_to_buffer.py @@ -9,6 +9,7 @@ from gi.repository import GtkSource from gi.repository import Gtk from gi.repository import Gdk from gi.repository import Gio +from gi.repository import GLib # Application imports from ..command_helpers import update_info_bar_if_focused @@ -35,6 +36,10 @@ def execute( update_info_bar_if_focused(view.command, view) view.emit("focus-in-event", Gdk.Event()) - buffer = view.get_buffer() - itr = buffer.get_iter_at_mark( buffer.get_insert() ) - view.scroll_to_iter(itr, 0.2, False, 0, 0) + def scroll_to_insert_itr(view): + buffer = view.get_buffer() + itr = buffer.get_iter_at_mark( buffer.get_insert() ) + view.scroll_to_iter(itr, 0.2, False, 0, 0) + + GLib.idle_add(scroll_to_insert_itr, view) +