diff --git a/plugins/code/completers/example_completer/provider_response_cache.py b/plugins/code/completers/example_completer/provider_response_cache.py index bc6d50b..6d0a45b 100644 --- a/plugins/code/completers/example_completer/provider_response_cache.py +++ b/plugins/code/completers/example_completer/provider_response_cache.py @@ -51,7 +51,10 @@ class ProviderResponseCache(ProviderResponseCacheBase): def process_file_save(self, event: Code_Event_Types.SavedFileEvent): ... - def process_file_change(self, event: Code_Event_Types.TextChangedEvent): + def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent): + ... + + def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent): ... def filter(self, word: str) -> list[dict]: diff --git a/plugins/code/completers/snippets_completer/provider_response_cache.py b/plugins/code/completers/snippets_completer/provider_response_cache.py index 7b0010c..e1d7800 100644 --- a/plugins/code/completers/snippets_completer/provider_response_cache.py +++ b/plugins/code/completers/snippets_completer/provider_response_cache.py @@ -49,7 +49,10 @@ class ProviderResponseCache(ProviderResponseCacheBase): def process_file_save(self, event: Code_Event_Types.SavedFileEvent): ... - def process_file_change(self, event: Code_Event_Types.TextChangedEvent): + def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent): + ... + + def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent): ... def filter(self, word: str) -> list[dict]: diff --git a/plugins/code/completers/words_completer/provider_response_cache.py b/plugins/code/completers/words_completer/provider_response_cache.py index 663fe95..55629e5 100644 --- a/plugins/code/completers/words_completer/provider_response_cache.py +++ b/plugins/code/completers/words_completer/provider_response_cache.py @@ -35,11 +35,14 @@ class ProviderResponseCache(ProviderResponseCacheBase): def process_file_save(self, event: Code_Event_Types.SavedFileEvent): ... - def process_file_change(self, event: Code_Event_Types.TextChangedEvent): - buffer = event.file.buffer + def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent): + buffer = event.buffer self._clear_temp_delay() self._set_temp_delay(buffer) + def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent): + ... + def _clear_temp_delay(self): if self._temp_timeout_id: GLib.source_remove(self._temp_timeout_id) diff --git a/plugins/code/language_server_clients/lsp_manager/client/lsp_client.py b/plugins/code/language_server_clients/lsp_manager/client/lsp_client.py index a36f069..7da8034 100644 --- a/plugins/code/language_server_clients/lsp_manager/client/lsp_client.py +++ b/plugins/code/language_server_clients/lsp_manager/client/lsp_client.py @@ -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 diff --git a/plugins/code/language_server_clients/lsp_manager/client/lsp_client_events.py b/plugins/code/language_server_clients/lsp_manager/client/lsp_client_events.py index df52d8e..8b7dd3e 100644 --- a/plugins/code/language_server_clients/lsp_manager/client/lsp_client_events.py +++ b/plugins/code/language_server_clients/lsp_manager/client/lsp_client_events.py @@ -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"] diff --git a/plugins/code/language_server_clients/lsp_manager/client/lsp_client_websocket.py b/plugins/code/language_server_clients/lsp_manager/client/lsp_client_websocket.py index 7b8fe57..1eb49ea 100644 --- a/plugins/code/language_server_clients/lsp_manager/client/lsp_client_websocket.py +++ b/plugins/code/language_server_clients/lsp_manager/client/lsp_client_websocket.py @@ -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) \ No newline at end of file + GLib.idle_add(self.handle_lsp_response, lsp_response) diff --git a/plugins/code/language_server_clients/lsp_manager/client/websocket_client.py b/plugins/code/language_server_clients/lsp_manager/client/websocket.py similarity index 95% rename from plugins/code/language_server_clients/lsp_manager/client/websocket_client.py rename to plugins/code/language_server_clients/lsp_manager/client/websocket.py index 65b673d..14ec3db 100644 --- a/plugins/code/language_server_clients/lsp_manager/client/websocket_client.py +++ b/plugins/code/language_server_clients/lsp_manager/client/websocket.py @@ -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) \ No newline at end of file + self.ws.run_forever(reconnect = 0.5) diff --git a/plugins/code/language_server_clients/lsp_manager/lsp_manager_client.py b/plugins/code/language_server_clients/lsp_manager/client_manager.py similarity index 72% rename from plugins/code/language_server_clients/lsp_manager/lsp_manager_client.py rename to plugins/code/language_server_clients/lsp_manager/client_manager.py index 9befbe5..6cb363a 100644 --- a/plugins/code/language_server_clients/lsp_manager/lsp_manager_client.py +++ b/plugins/code/language_server_clients/lsp_manager/client_manager.py @@ -4,14 +4,15 @@ from concurrent.futures import ThreadPoolExecutor # Lib imports # Application imports -from .mixins.lsp_client_events_mixin import LSPClientEventsMixin +from .config import get_lsp_connect_timout +from .mixins.client_manager_events_mixin import ClientManagerEventsMixin from .client.lsp_client import LSPClient -class LSPManagerClient(LSPClientEventsMixin): +class ClientManager(ClientManagerEventsMixin): def __init__(self): - super(LSPManagerClient, self).__init__() + super(ClientManager, self).__init__() self._cache_refresh_timeout_id: int = None @@ -24,13 +25,13 @@ class LSPManagerClient(LSPClientEventsMixin): self, lang_id: str, workspace_path: str, - init_opts: dict[str, str] + init_opts: dict[str, str], + address: str = "127.0.0.1", + port: str = "9999" ) -> LSPClient: if lang_id in self.clients: return None - address = "127.0.0.1" - port = 9999 - uri = f"ws://{address}:{port}/{lang_id}" + uri = f"ws://{address}:{port}/{lang_id}?workspace={workspace_path}" client = LSPClient() client.set_socket(uri) @@ -39,7 +40,7 @@ class LSPManagerClient(LSPClientEventsMixin): client.set_init_opts(init_opts) client.start_client() - if not client.ws_client.wait_for_connection(timeout = 5.0): + if not client.websocket.wait_for_connection(timeout = get_lsp_connect_timout()): logger.error(f"Failed to connect to LSP server for {lang_id}") return None diff --git a/plugins/code/language_server_clients/lsp_manager/commands.py b/plugins/code/language_server_clients/lsp_manager/commands.py new file mode 100644 index 0000000..f8cfed3 --- /dev/null +++ b/plugins/code/language_server_clients/lsp_manager/commands.py @@ -0,0 +1,87 @@ +# Python imports + +# Lib imports +import gi + +gi.require_version('Gtk', '3.0') +gi.require_version('GtkSource', '4') + +from gi.repository import Gtk +from gi.repository import GtkSource + +# Application imports + + + +class Commands: + lsp_manager: callable = None + + class lsp_manager_toggle: + @staticmethod + def execute( + source_view: GtkSource, + char_str: str, + modkeys_states: tuple + ): + logger.debug("Command: LSP Manager Toggle") + if Commands.lsp_manager.ui_manager.is_visible(): + Commands.lsp_manager.ui_manager.hide() + else: + Commands.lsp_manager.ui_manager.show() + + class lsp_references: + @staticmethod + def execute( + view: GtkSource, + char_str: str, + modkeys_states: tuple + ): + logger.debug("Command: LSP References") + + file = view.command.exec("get_current_file") + buffer = view.get_buffer() + iter = buffer.get_iter_at_mark( buffer.get_insert() ) + line = iter.get_line() + column = iter.get_line_offset() + + Commands.lsp_manager.client_manager.process_references_definition( + file.ftype, file.fpath, line, column + ) + + class lsp_implementation: + @staticmethod + def execute( + view: GtkSource, + char_str: str, + modkeys_states: tuple + ): + logger.debug("Command: LSP Implements") + + file = view.command.exec("get_current_file") + buffer = view.get_buffer() + iter = buffer.get_iter_at_mark( buffer.get_insert() ) + line = iter.get_line() + column = iter.get_line_offset() + + Commands.lsp_manager.client_manager.process_implementation_definition( + file.ftype, file.fpath, line, column + ) + + class lsp_definition: + @staticmethod + def execute( + view: GtkSource, + char_str: str, + modkeys_states: tuple + ): + logger.debug("Command: LSP Definition (Go-To)") + + file = view.command.exec("get_current_file") + buffer = view.get_buffer() + iter = buffer.get_iter_at_mark( buffer.get_insert() ) + line = iter.get_line() + column = iter.get_line_offset() + + Commands.lsp_manager.client_manager.process_definition( + file.ftype, file.fpath, line, column + ) diff --git a/plugins/code/language_server_clients/lsp_manager/config.py b/plugins/code/language_server_clients/lsp_manager/config.py new file mode 100644 index 0000000..06311af --- /dev/null +++ b/plugins/code/language_server_clients/lsp_manager/config.py @@ -0,0 +1,38 @@ +# Python imports +from os import path +import json + +# Lib imports + +# Application imports + + + +LSP_HOST: str = "127.0.0.1" +LSP_PORT: int = 9999 +LSP_CONNECT_TIMOUT: float = 5.0 + + + +def get_lsp_host_addr() -> str: + return LSP_HOST + +def get_lsp_host_port() -> int: + return LSP_PORT + +def get_lsp_connect_timout() -> float: + return LSP_CONNECT_TIMOUT + +def get_lsp_init_config() -> 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() + return json.loads(data) + except Exception as e: + logger.error( f"LSP Controller: {_LSP_INIT_CONFIG}\n\t\t{repr(e)}" ) + + return {} diff --git a/plugins/code/language_server_clients/lsp_manager/configs/lsp-servers-config.json b/plugins/code/language_server_clients/lsp_manager/configs/reference-lsp-server-configs.json similarity index 100% rename from plugins/code/language_server_clients/lsp_manager/configs/lsp-servers-config.json rename to plugins/code/language_server_clients/lsp_manager/configs/reference-lsp-server-configs.json diff --git a/plugins/code/language_server_clients/lsp_manager/dto/code/lsp/lsp_messages.py b/plugins/code/language_server_clients/lsp_manager/dto/code/lsp/lsp_messages.py index 81117a4..ec28b03 100644 --- a/plugins/code/language_server_clients/lsp_manager/dto/code/lsp/lsp_messages.py +++ b/plugins/code/language_server_clients/lsp_manager/dto/code/lsp/lsp_messages.py @@ -96,10 +96,10 @@ didchange_notification_range = { "uri": "file://", "languageId": "python", "version": 1, - "text": "" }, "contentChanges": [ { + "text": "", "range": { "start": { "line": 1, @@ -108,9 +108,8 @@ didchange_notification_range = { "end": { "line": 1, "character": 1, - }, - "rangeLength": 0 - } + } + }, } ] } @@ -125,19 +124,11 @@ completion_request = { "method": "textDocument/completion", "params": { "textDocument": { - "uri": "file://", - "languageId": "python", - "version": 1, - "text": "" + "uri": "file://" }, "position": { "line": 5, - "character": 12, - "offset": 0 - }, - "contet": { - "triggerKind": 3, - "triggerCharacter": "" + "character": 12 } } } @@ -159,6 +150,19 @@ definition_request = { } } +implementation_request = { + "method": "textDocument/implementation", + "params": { + "textDocument": { + "uri": "file://" + }, + "position": { + "line": 5, + "character": 12 + } + } +} + references_request = { "method": "textDocument/references", "params": { @@ -190,4 +194,4 @@ symbols_request = { "text": "" } } -} \ No newline at end of file +} diff --git a/plugins/code/language_server_clients/lsp_manager/lsp_manager.py b/plugins/code/language_server_clients/lsp_manager/lsp_manager.py index a806b89..eb0b8e1 100644 --- a/plugins/code/language_server_clients/lsp_manager/lsp_manager.py +++ b/plugins/code/language_server_clients/lsp_manager/lsp_manager.py @@ -11,10 +11,11 @@ from .dto.code.events import \ from .dto.code.lsp.lsp_message_structs import \ LSPResponseTypes, LSPResponseRequest, LSPResponseNotification +from .ui_manager import UIManager + from .provider import Provider from .provider_response_cache import ProviderResponseCache -from .lsp_manager_ui import LSPManagerUI -from .lsp_manager_client import LSPManagerClient +from .client_manager import ClientManager from .response_handlers.response_registry import ResponseRegistry @@ -31,16 +32,16 @@ class LSPManager(ControllerBase): def _init(self): self.provider: Provider = Provider() self.response_cache: ProviderResponseCache = ProviderResponseCache() - self.lsp_manager_client: LSPManagerClient = LSPManagerClient() + self.client_manager: ClientManager = ClientManager() self.response_registry: ResponseRegistry = ResponseRegistry() def _load_widgets(self): - self.lsp_manager_ui: LSPManagerUI = LSPManagerUI() - self.lsp_manager_ui.connect('create-client', self._on_create_client) - self.lsp_manager_ui.connect('close-client', self._on_close_client) + self.ui_manager: LSPManagerUI = UIManager() + self.ui_manager.connect('create-client', self._on_create_client) + self.ui_manager.connect('close-client', self._on_close_client) def _do_bind_mapping(self): - self.response_cache.set_lsp_manager_client(self.lsp_manager_client) + self.response_cache.set_lsp_manager_client(self.client_manager) self.provider.response_cache = self.response_cache self.response_registry.set_event_hub( self.emit, self.emit_to, self.provider @@ -49,14 +50,20 @@ class LSPManager(ControllerBase): def _controller_message(self, event: Code_Event_Types.CodeEvent): if isinstance(event, Code_Event_Types.RegisterLspClientEvent): self.response_registry.register_handler(event.lang_id, event.handler) - self.lsp_manager_ui.add_client_listing(event.lang_id, event.lang_config) + self.ui_manager.add_client_listing(event.lang_id, event.lang_config) elif isinstance(event, Code_Event_Types.UnregisterLspClientEvent): self.response_registry.unregister_handler(event.lang_id) - self.lsp_manager_ui.remove_client_listing(event.lang_id) + self.ui_manager.remove_client_listing(event.lang_id) def _on_create_client(self, ui, lang_id: str, workspace_path: str) -> bool: init_opts = ui.get_init_opts(lang_id) - result = self.create_client(lang_id, workspace_path, init_opts) + result = self.create_client( + lang_id, + workspace_path, + init_opts, + ui.adddress_entry.get_text(), + f"{ int( ui.adddress_port.get_value() ) }" + ) if result: ui.toggle_client_buttons(show_close = True) @@ -72,20 +79,22 @@ class LSPManager(ControllerBase): return result def handle_destroy(self): - self.lsp_manager_ui.disconnect_by_func(self._on_create_client) - self.lsp_manager_ui.disconnect_by_func(self._on_close_client) + self.ui_manager.disconnect_by_func(self._on_create_client) + self.ui_manager.disconnect_by_func(self._on_close_client) def create_client( self, lang_id: str, workspace_path: str, - init_opts: dict[str, str] + init_opts: dict[str, str], + address: str, + port: str ) -> bool: - client = self.lsp_manager_client.create_client( - lang_id, workspace_path, init_opts + client = self.client_manager.create_client( + lang_id, workspace_path, init_opts, address, port ) handler = self.response_registry.get_handler(lang_id) - self.lsp_manager_client.active_language_id = lang_id + self.client_manager.active_language_id = lang_id if not client or not handler: logger.error(f"LSP Manager: Either 'client' or 'handler' didn't get created...'") @@ -96,28 +105,38 @@ class LSPManager(ControllerBase): handler.set_response_cache(self.response_cache) client.handle_lsp_response = self.server_response - client.send_initialize_message() return True def close_client(self, lang_id: str) -> bool: - self.lsp_manager_client.close_client(lang_id) + self.client_manager.close_client(lang_id) self.response_registry.close_handler(lang_id) return True - def server_response(self, lsp_response: LSPResponseTypes): + def server_response(self, lsp_response: LSPResponseTypes | dict): logger.debug(f"LSP Response: { lsp_response }") - if isinstance(lsp_response, LSPResponseRequest): - if not self.lsp_manager_client.active_language_id in self.lsp_manager_client.clients: - logger.debug(f"No LSP client for '{self.lsp_manager_client.active_language_id}', skipping 'server_response'") + if isinstance(lsp_response, dict): + if not self.client_manager.active_language_id in self.client_manager.clients: + logger.debug(f"No LSP client for '{self.client_manager.active_language_id}', skipping 'server_response'") return - - controller = self.lsp_manager_client.get_active_client() + + controller = self.client_manager.get_active_client() + if "type" in lsp_response and lsp_response["type"] == "connected": + controller.send_initialize_message() + + return + + if isinstance(lsp_response, LSPResponseRequest): + if not self.client_manager.active_language_id in self.client_manager.clients: + logger.debug(f"No LSP client for '{self.client_manager.active_language_id}', skipping 'server_response'") + return + + controller = self.client_manager.get_active_client() event = controller.get_event_by_id(lsp_response.id) handler = self.response_registry.get_handler( - self.lsp_manager_client.active_language_id, event + self.client_manager.active_language_id, event ) if not handler: return diff --git a/plugins/code/language_server_clients/lsp_manager/lsp_manager_ui.py b/plugins/code/language_server_clients/lsp_manager/lsp_manager_ui.py deleted file mode 100644 index 47c84c0..0000000 --- a/plugins/code/language_server_clients/lsp_manager/lsp_manager_ui.py +++ /dev/null @@ -1,234 +0,0 @@ -# Python imports -from os import path -import json - -# Lib imports -import gi -gi.require_version('Gtk', '3.0') -gi.require_version('GtkSource', '4') - -from gi.repository import GObject -from gi.repository import Gtk -from gi.repository import GLib -from gi.repository import GtkSource - -# Application imports - - - -class LSPManagerUI(Gtk.Dialog): - __gsignals__ = { - 'create-client': (GObject.SignalFlags.RUN_LAST, None, (str, str)), - 'close-client': (GObject.SignalFlags.RUN_LAST, None, (str,)), - } - - def __init__(self): - super(LSPManagerUI, self).__init__() - - self._USER_HOME = path.expanduser('~') - - self.client_configs: dict[str, str] = {} - - self.source_view = None - - self._setup_styling() - self._setup_signals() - self._subscribe_to_events() - self._load_widgets() - - - def _setup_styling(self): - self.set_modal(True) - self.set_decorated(False) - self.set_vexpand(True) - self.set_hexpand(True) - - def _setup_signals(self): - self.connect("show", self._handle_show) - self.connect("destroy", self._handle_destroy) - - def _subscribe_to_events(self): - ... - - def _load_widgets(self): - content_area = self.get_content_area() - self.main_box = Gtk.Grid() - self.path_entry = Gtk.SearchEntry() - self.path_bttn = Gtk.FileChooserButton.new( - title = "Workspace Folder", - action = Gtk.FileChooserAction.SELECT_FOLDER - ) - self.combo_box = Gtk.ComboBoxText() - - self.hide_bttn = Gtk.Button(label = "X") - bttn_box = Gtk.Box() - self.create_client_bttn = Gtk.Button(label = "Create Language Client") - self.close_client_bttn = Gtk.Button(label = "Close Language Client") - - self.path_entry.set_can_focus(False) - self.path_entry.set_placeholder_text("Workspace Folder...") - self.path_entry.connect("changed", self._path_changed, bttn_box) - self.path_bttn.set_halign(Gtk.Align.FILL) - - self.path_bttn.connect("file-set", self._file_set) - self.combo_box.connect("changed", self._on_combo_changed) - self.hide_bttn_id = self.hide_bttn.connect("clicked", lambda widget: self.hide()) - self.create_client_bttn.connect("clicked", self._create_client, self.close_client_bttn) - self.close_client_bttn.connect("clicked", self._close_client, self.create_client_bttn) - - self.main_box.set_column_spacing(15) - self.main_box.set_row_spacing(15) - - bttn_box.pack_start(self.create_client_bttn, False, False, 0) - bttn_box.pack_start(self.close_client_bttn, False, False, 0) - - self.main_box.attach(child = self.path_entry, left = 0, top = 0, width = 4, height = 1) - self.main_box.attach(child = self.path_bttn, left = 4, top = 0, width = 1, height = 1) - self.main_box.attach(child = self.combo_box, left = 5, top = 0, width = 1, height = 1) - self.main_box.attach(child = self.hide_bttn, left = 6, top = 0, width = 1, height = 1) - self.main_box.attach(child = bttn_box, left = 0, top = 1, width = 1, height = 1) - - content_area.set_vexpand(True) - content_area.set_hexpand(True) - - content_area.add(self.main_box) - content_area.show_all() - self.close_client_bttn.hide() - bttn_box.hide() - - def _handle_show(self, widget): - GLib.idle_add(self.path_entry.grab_focus) - - def _handle_destroy(self, widget): - self.disconnect_by_func(self._show) - self.disconnect_by_func(self._handle_destroy) - self.path_bttn.disconnect_by_func(self._file_set) - self.combo_box.disconnect_by_func(self._on_combo_changed) - self.hide_bttn.disconnect(self.hide_bttn_id) - self.create_client_bttn.disconnect_by_func(self._create_client) - self.close_client_bttn.disconnect_by_func(self._close_client) - - def _map_resize(self, widget, parent): - parent_x, parent_y = parent.get_position() - parent_width, parent_height = parent.get_size() - if parent_width == 0 or parent_height == 0: return - - width = int(parent_width * 0.75) - height = int(parent_height * 0.75) - - widget.resize(width, height) - - x = parent_x + (parent_width - width) // 2 - y = parent_y + (parent_height - height) // 2 - widget.move(x, y) - - def _path_changed(self, widget, buttons_widget): - if not widget.get_text(): - self.path_bttn.unselect_all() - self.path_bttn.emit("file-set") - buttons_widget.hide() - return - - self.set_source_view_text( self.path_entry.get_text() ) - buttons_widget.show() - - def _file_set(self, widget): - fname = widget.get_filename() - fname = "" if not fname else fname - self.path_entry.set_text(fname) - - lang_id = self.combo_box.get_active_text() - if not lang_id or lang_id not in self.client_configs: return - - self.set_source_view_text( - "{workspace.folder}" if not fname else fname - ) - - 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 - - workspace_dir = self.path_entry.get_text() - self.emit('create-client', lang_id, workspace_dir) - - def _close_client(self, widget, sibling): - lang_id = self.combo_box.get_active_text() - - if not lang_id: return - self.emit('close-client', lang_id) - - def _on_combo_changed(self, combo: Gtk.ComboBoxText): - lang_id = combo.get_active_text() - self.set_source_view_text( self.path_entry.get_text() ) - - - def set_source_view_text(self, workspace_dir: str): - lang_id = self.combo_box.get_active_text() - if not lang_id: return - - json_str = self.client_configs[lang_id] \ - .replace("{workspace.folder}", workspace_dir) \ - .replace("{user.home}", self._USER_HOME) - - buffer = self.source_view.get_buffer() - buffer.set_text(json_str, -1) - - def map_parent_resize_event(self, parent): - self.size_allocate_id = parent.connect("size-allocate", lambda w, r: self._map_resize(self, parent)) - - def unmap_parent_resize_event(self, parent): - parent.disconnect(self.size_allocate_id) - - def set_source_view(self, source_view): - scrolled_win = Gtk.ScrolledWindow() - lang_manager = GtkSource.LanguageManager() - buffer = source_view.get_buffer() - language = lang_manager.get_language("json") - self.source_view = source_view - - buffer.set_language(language) - buffer.set_style_scheme(self.source_view.syntax_theme) - - scrolled_win.set_hexpand(True) - scrolled_win.set_vexpand(True) - - scrolled_win.add(self.source_view) - self.main_box.attach(child = scrolled_win, left = 0, top = 2, width = 7, height = 1) - - scrolled_win.show_all() - - def add_client_listing(self, lang_id: str, lang_config: str): - self.combo_box.append_text(lang_id) - self.client_configs[lang_id] = lang_config - - def remove_client_listing(self, lang_id: str): - model = self.combo_box.get_model() - - for i, row in enumerate(model): - if row[0] == lang_id: - self.combo_box.remove(i) - break - - if lang_id in self.client_configs: - del self.client_configs[lang_id] - - def get_init_opts(self, lang_id: str) -> dict: - if not lang_id or lang_id not in self.client_configs: return {} - - try: - buffer = self.source_view.get_buffer() - json_str = buffer.get_text(*buffer.get_bounds(), -1) - lang_config = json.loads(json_str) - except json.JSONDecodeError as e: - logger.error(f"Invalid JSON for {lang_id}: {e}") - return {} - - return lang_config.get("initialization-options", {}) - - def toggle_client_buttons(self, show_close: bool): - self.create_client_bttn.set_visible(not show_close) - self.close_client_bttn.set_visible(show_close) diff --git a/plugins/code/language_server_clients/lsp_manager/mixins/client_manager_events_mixin.py b/plugins/code/language_server_clients/lsp_manager/mixins/client_manager_events_mixin.py new file mode 100644 index 0000000..6a1e152 --- /dev/null +++ b/plugins/code/language_server_clients/lsp_manager/mixins/client_manager_events_mixin.py @@ -0,0 +1,191 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.event_factory import Code_Event_Types + + + +class ClientManagerEventsMixin: + def _get_controller(self, lang_id, action: str): + controller = self.clients.get(lang_id) + if not controller: + logger.debug(f"No LSP client for '{lang_id}', skipping {action}...") + + return controller + + def _uri(self, fpath: str) -> str: + return fpath if fpath.startswith("file://") else f"file://{fpath}" + + def _text(self, buffer, *, hidden = False): + return buffer.get_text( + *buffer.get_bounds(), + include_hidden_chars=hidden + ) + + def _version(self, controller, uri, bump = False): + if bump: + controller.doc_vers[uri] = controller.doc_vers.get(uri, -1) + 1 + + return controller.doc_vers.get(uri, 0) + + def _activate(self, lang_id): + self.active_language_id = lang_id + + def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent): + f = event.file + if not (c := self._get_controller(f.ftype, "didOpen")): return + + uri = self._uri(f.fpath) + self._activate(f.ftype) + + c._lsp_did_open({ + "uri": uri, + "language_id": f.ftype, + "text": self._text(f.buffer), + }) + + def process_file_close(self, event: Code_Event_Types.RemovedFileEvent): + f = event.file + if not (c := self._get_controller(f.ftype, "didClose")): return + + uri = self._uri(f.fpath) + c.doc_vers.pop(uri, None) + c._lsp_did_close({"uri": uri}) + + def process_file_save(self, event: Code_Event_Types.SavedFileEvent): + f = event.file + if not (c := self._get_controller(f.ftype, "didSave")): return + + uri = self._uri(f.fpath) + self._activate(f.ftype) + + c._lsp_did_save({ + "uri": uri, + "text": self._text(f.buffer), + }) + + def process_file_change(self, event: Code_Event_Types.TextChangedEvent): + f = event.file + if not (c := self._get_controller(f.ftype, "didChange")): return + + uri = self._uri(f.fpath) + self._activate(f.ftype) + + version = self._version(c, uri, bump = True) + + c._lsp_did_change({ + "uri": uri, + "language_id": f.ftype, + "version": version, + "text": self._text(f.buffer, hidden = True), + }) + + it = f.buffer.get_iter_at_mark(f.buffer.get_insert()) + self._set_cache_refresh_trigger( + f.ftype, f.fpath, + it.get_line(), + it.get_line_offset() + 1 + ) + + def _iter_pos(self, it): + return it.get_line(), it.get_line_offset() + + def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent): + f = event.file + if not (c := self._get_controller(f.ftype, "didChange")): return + + uri = self._uri(f.fpath) + self._activate(f.ftype) + + start_it = event.location.copy() + end_it = event.location.copy() + + if event.length > 1: + start_it.backward_chars(event.length) + + sl, sc = self._iter_pos(start_it) + el, ec = self._iter_pos(end_it) + sc -= 0 if event.length > 1 else 1 + ec -= 1 + + version = self._version(c, uri, bump = True) + + c._lsp_did_change_range({ + "uri": uri, + "language_id": f.ftype, + "version": version, + "text": event.text, + "line": sl, + "column": sc, + "end_line": el, + "end_column": ec, + }) + + it = event.buffer.get_iter_at_mark(event.buffer.get_insert()) + self._set_cache_refresh_trigger( + f.ftype, f.fpath, *self._iter_pos(it) + ) + + def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent): + f = event.file + if not (c := self._get_controller(f.ftype, "didChange")): return + + uri = self._uri(f.fpath) + self._activate(f.ftype) + + start_it, end_it = event.start.copy(), event.end.copy() + if start_it.compare(end_it) > 0: + start_it, end_it = end_it, start_it + + sl, sc = self._iter_pos(start_it) + el, ec = self._iter_pos(end_it) + + version = self._version(c, uri, bump = True) + + c._lsp_did_change_range({ + "uri": uri, + "language_id": f.ftype, + "version": version, + "text": "", + "line": sl, + "column": sc, + "end_line": el, + "end_column": ec, + }) + + it = event.buffer.get_iter_at_mark(event.buffer.get_insert()) + self._set_cache_refresh_trigger( + f.ftype, f.fpath, *self._iter_pos(it) + ) + + def _request(self, method, lang_id, fpath, **extra): + if not (c := self._get_controller(lang_id, method)): return + + uri = self._uri(fpath) + self._activate(lang_id) + + payload = { + "uri": uri, + "language_id": lang_id, + "version": self._version(c, uri), + **extra + } + + getattr(c, method)(payload) + + def process_definition(self, lang_id, fpath, line, column): + self._request("_lsp_definition", lang_id, fpath, line = line, column = column) + + def process_implementation_definition(self, lang_id, fpath, line, column): + self._request("_lsp_implementation", lang_id, fpath, line = line, column = column) + + def process_references_definition(self, lang_id, fpath, line, column): + self._request("_lsp_references", lang_id, fpath, line = line, column = column) + + def process_completion_request(self, lang_id, fpath, line, column): + self._request("_lsp_completion", lang_id, fpath, line = line, column = column) + + def _set_cache_refresh_trigger(self, lang_id, fpath, line, column): + self.process_completion_request(lang_id, fpath, line, column) diff --git a/plugins/code/language_server_clients/lsp_manager/mixins/lsp_client_events_mixin.py b/plugins/code/language_server_clients/lsp_manager/mixins/lsp_client_events_mixin.py deleted file mode 100644 index 2344716..0000000 --- a/plugins/code/language_server_clients/lsp_manager/mixins/lsp_client_events_mixin.py +++ /dev/null @@ -1,144 +0,0 @@ -# Python imports - -# Lib imports -import gi - -from gi.repository import GLib - -# Application imports -from libs.event_factory import Code_Event_Types - - - -class LSPClientEventsMixin: - - def process_file_load(self, event: Code_Event_Types.AddedNewFileEvent): - lang_id = event.file.ftype - if lang_id not in self.clients: - logger.debug(f"No LSP client for '{lang_id}', skipping didOpen") - return - - controller = self.clients[lang_id] - fpath = event.file.fpath - uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath - buffer = event.file.buffer - text = buffer.get_text(*buffer.get_bounds()) - self.active_language_id = lang_id - - controller._lsp_did_open({ - "uri": uri, - "language_id": lang_id, - "text": text - }) - - def process_file_close(self, event: Code_Event_Types.RemovedFileEvent): - lang_id = event.file.ftype - if lang_id not in self.clients: - logger.debug(f"No LSP client for '{lang_id}', skipping didClose") - return - - controller = self.clients[lang_id] - fpath = event.file.fpath - uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath - - controller._lsp_did_close({"uri": uri}) - - def process_file_save(self, event: Code_Event_Types.SavedFileEvent): - lang_id = event.file.ftype - if lang_id not in self.clients: - logger.debug(f"No LSP client for '{lang_id}', skipping didSave") - return - - controller = self.clients[lang_id] - fpath = event.file.fpath - uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath - buffer = event.file.buffer - text = buffer.get_text(*buffer.get_bounds()) - self.active_language_id = lang_id - - controller._lsp_did_save({"uri": uri, "text": text}) - - def process_file_change(self, event: Code_Event_Types.TextChangedEvent): - self._clear_delayed_cache_refresh_trigger() - - lang_id = event.file.ftype - if lang_id not in self.clients: - logger.debug(f"No LSP client for '{lang_id}', skipping didChange") - return - - controller = self.clients[lang_id] - fpath = event.file.fpath - uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath - buffer = event.file.buffer - text = buffer.get_text(*buffer.get_bounds()) - self.active_language_id = lang_id - - controller._lsp_did_change({ - "uri": uri, - "language_id": lang_id, - "version": 1, - "text": text - }) - - iter = buffer.get_iter_at_mark( buffer.get_insert() ) - line = iter.get_line() - column = iter.get_line_offset() - self._set_cache_refresh_trigger( - lang_id, fpath, line, column - ) - - - def process_goto_definition( - self, lang_id: str, fpath: str, line: int, column: int - ): - if lang_id not in self.clients: - logger.debug(f"No LSP client for '{lang_id}', skipping goto definition") - return - - controller = self.clients[lang_id] - uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath - self.active_language_id = lang_id - - controller._lsp_definition({ - "uri": uri, - "language_id": lang_id, - "version": 1, - "line": line, - "column": column - }) - - def process_completion_request( - self, lang_id: str, fpath: str, line: int, column: int - ): - if lang_id not in self.clients: - logger.debug(f"No LSP client for '{lang_id}', skipping completion") - return - - controller = self.clients[lang_id] - uri = f"file://{fpath}" if not fpath.startswith("file://") else fpath - self.active_language_id = lang_id - - controller._lsp_completion({ - "uri": uri, - "language_id": lang_id, - "version": 1, - "line": line, - "column": column - }) - - - def _clear_delayed_cache_refresh_trigger(self): - if self._cache_refresh_timeout_id: - GLib.source_remove(self._cache_refresh_timeout_id) - - def _set_cache_refresh_trigger( - self, lang_id: str, fpath: str, line: int, column: int - ): - def trigger_cache_refresh(lang_id, fpath, line, column): - self._cache_refresh_timeout_id = None - self.process_completion_request( - lang_id, fpath, line, column - ) - return False - - self._cache_refresh_timeout_id = GLib.timeout_add(1500, trigger_cache_refresh, lang_id, fpath, line, column) diff --git a/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_clients_mixin.py b/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_clients_mixin.py new file mode 100644 index 0000000..ad15b3c --- /dev/null +++ b/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_clients_mixin.py @@ -0,0 +1,72 @@ +# Python imports +import json + +# Lib imports +import gi + +from gi.repository import GObject + +# Application imports + + + +class UIManagerClientsMixin: + 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 + + workspace_dir = self.path_entry.get_text() + self.emit('create-client', lang_id, workspace_dir) + + def _close_client(self, widget, sibling): + lang_id = self.combo_box.get_active_text() + + if not lang_id: return + + self.emit('close-client', lang_id) + + def set_source_view_text(self, workspace_dir: str): + lang_id = self.combo_box.get_active_text() + + if not lang_id: return + + json_str = self.client_configs[lang_id]\ + .replace("{workspace.folder}", workspace_dir)\ + .replace("{user.home}", self._USER_HOME) + + self.source_view.get_buffer().set_text(json_str, -1) + + def add_client_listing(self, lang_id: str, lang_config: str): + self.combo_box.append_text(lang_id) + self.client_configs[lang_id] = lang_config + + def remove_client_listing(self, lang_id: str): + model = self.combo_box.get_model() + + for i, row in enumerate(model): + if row[0] == lang_id: + self.combo_box.remove(i) + break + + self.client_configs.pop(lang_id, None) + + def toggle_client_buttons(self, show_close: bool): + self.create_client_bttn.set_visible(not show_close) + self.close_client_bttn.set_visible(show_close) + + def get_init_opts(self, lang_id: str) -> dict: + if not lang_id or lang_id not in self.client_configs: return {} + + try: + buffer = self.source_view.get_buffer() + json_str = buffer.get_text(*buffer.get_bounds(), -1) + lang_config = json.loads(json_str) + except json.JSONDecodeError as e: + logger.error(f"Invalid JSON for {lang_id}: {e}") + return {} + + return lang_config.get("initialization-options", {}) diff --git a/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_events_mixin.py b/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_events_mixin.py new file mode 100644 index 0000000..f4a5181 --- /dev/null +++ b/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_events_mixin.py @@ -0,0 +1,77 @@ +# Python imports + +# Lib imports +import gi + +gi.require_version("Gtk", "3.0") + +from gi.repository import GLib +from gi.repository import Gtk + +# Application imports + + + +class UIManagerEventsMixin: + def _setup_signals(self): + self.connect("show", self._handle_show) + self.connect("destroy", self._handle_destroy) + + def _subscribe_to_events(self): + ... + + def _handle_show(self, widget): + GLib.idle_add(self.path_entry.grab_focus) + + def _handle_destroy(self, widget): + self.disconnect_by_func(self._handle_show) + self.disconnect_by_func(self._handle_destroy) + self.path_bttn.disconnect_by_func(self._file_set) + self.combo_box.disconnect_by_func(self._on_combo_changed) + self.hide_bttn.disconnect(self.hide_bttn_id) + self.create_client_bttn.disconnect_by_func(self._create_client) + self.close_client_bttn.disconnect_by_func(self._close_client) + + def _map_resize(self, widget, parent): + parent_x, \ + parent_y = parent.get_position() + parent_width, \ + parent_height = parent.get_size() + + if parent_width == 0 or parent_height == 0: return + + width = int(parent_width * 0.75) + height = int(parent_height * 0.75) + + widget.resize(width, height) + + x = parent_x + (parent_width - width) // 2 + y = parent_y + (parent_height - height) // 2 + widget.move(x, y) + + def _path_changed(self, widget, buttons_widget): + if not widget.get_text(): + self.path_bttn.unselect_all() + self.path_bttn.emit("file-set") + buttons_widget.hide() + return + + self.set_source_view_text( self.path_entry.get_text() ) + buttons_widget.show() + + def _file_set(self, widget): + fname = widget.get_filename() + fname = "" if not fname else fname + self.path_entry.set_text(fname) + + lang_id = self.combo_box.get_active_text() + if not lang_id or lang_id not in self.client_configs: return + + self.set_source_view_text( + "{workspace.folder}" if not fname else fname + ) + + def _on_combo_changed(self, combo: Gtk.ComboBoxText): + lang_id = combo.get_active_text() + self.set_source_view_text( self.path_entry.get_text() ) + diff --git a/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_setup_mixin.py b/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_setup_mixin.py new file mode 100644 index 0000000..ad81d12 --- /dev/null +++ b/plugins/code/language_server_clients/lsp_manager/mixins/ui_manager_setup_mixin.py @@ -0,0 +1,98 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('GtkSource', '4') + +from gi.repository import Gtk +from gi.repository import GtkSource + +# Application imports +from ..config import get_lsp_host_addr, get_lsp_host_port + + + +class UIManagerSetupMixin: + def _setup_styling(self): + self.set_modal(True) + self.set_decorated(False) + self.set_vexpand(True) + self.set_hexpand(True) + + def _load_widgets(self): + content_area = self.get_content_area() + self.main_box = Gtk.Grid() + self.path_entry = Gtk.SearchEntry() + self.path_bttn = Gtk.FileChooserButton.new( + title = "Workspace Folder", + action = Gtk.FileChooserAction.SELECT_FOLDER + ) + self.combo_box = Gtk.ComboBoxText() + self.hide_bttn = Gtk.Button(label = "X") + + self.adddress_entry = Gtk.Entry() + adjustment = Gtk.Adjustment( + value = get_lsp_host_port(), + lower = 1, + upper = 65535, + step_increment = 1, + page_increment = 10, + page_size = 0 + ) + + self.adddress_port = Gtk.SpinButton() + self.adddress_port.set_adjustment(adjustment) + self.adddress_port.set_digits(0) # integers only + + bttn_box = Gtk.Box() + self.create_client_bttn = Gtk.Button(label = "Create Language Client") + self.close_client_bttn = Gtk.Button(label = "Close Language Client") + + self.path_entry.set_can_focus(False) + self.path_entry.set_placeholder_text("Workspace Folder...") + self.path_entry.connect("changed", self._path_changed, bttn_box) + self.path_bttn.set_halign(Gtk.Align.FILL) + + self.adddress_entry.set_placeholder_text("Address...") + self.adddress_entry.set_text( get_lsp_host_addr() ) + + self.path_bttn.connect("file-set", self._file_set) + self.combo_box.connect("changed", self._on_combo_changed) + self.hide_bttn_id = self.hide_bttn.connect("clicked", lambda widget: self.hide()) + self.create_client_bttn.connect("clicked", self._create_client, self.close_client_bttn) + self.close_client_bttn.connect("clicked", self._close_client, self.create_client_bttn) + + self.main_box.set_column_spacing(15) + self.main_box.set_row_spacing(15) + + bttn_box.pack_start(self.create_client_bttn, False, False, 0) + bttn_box.pack_start(self.close_client_bttn, False, False, 0) + + self.main_box.attach(child = self.path_entry, left = 0, top = 0, width = 4, height = 1) + self.main_box.attach(child = self.path_bttn, left = 4, top = 0, width = 1, height = 1) + self.main_box.attach(child = self.combo_box, left = 5, top = 0, width = 1, height = 1) + self.main_box.attach(child = self.hide_bttn, left = 6, top = 0, width = 1, height = 1) + + self.main_box.attach(child = self.adddress_entry, left = 0, top = 1, width = 2, height = 1) + self.main_box.attach(child = self.adddress_port, left = 2, top = 1, width = 2, height = 1) + self.main_box.attach(child = bttn_box, left = 4, top = 1, width = 3, height = 1) + + content_area.set_vexpand(True) + content_area.set_hexpand(True) + + content_area.add(self.main_box) + content_area.show_all() + self.close_client_bttn.hide() + bttn_box.hide() + + def set_source_view(self, scrolled_win, source_view): + lang_manager = GtkSource.LanguageManager() + buffer = source_view.get_buffer() + language = lang_manager.get_language("json") + self.source_view = source_view + + buffer.set_language(language) + buffer.set_style_scheme(self.source_view.syntax_theme) + + self.main_box.attach(scrolled_win, 0, 2, 7, 1) diff --git a/plugins/code/language_server_clients/lsp_manager/plugin.py b/plugins/code/language_server_clients/lsp_manager/plugin.py index 4f8612b..c2643b3 100644 --- a/plugins/code/language_server_clients/lsp_manager/plugin.py +++ b/plugins/code/language_server_clients/lsp_manager/plugin.py @@ -12,6 +12,7 @@ from libs.dto.states import SourceViewStates from plugins.plugin_types import PluginCode from .dto.code import events as lsp_events +from .commands import Commands from .lsp_manager import LSPManager @@ -35,23 +36,11 @@ class Plugin(PluginCode): window = self.request_ui_element("main-window") - lsp_manager.lsp_manager_ui.map_parent_resize_event(window) + lsp_manager.ui_manager.map_parent_resize_event(window) - event = Event_Factory.create_event("register_command", - command_name = "LSP Manager", - command = Handler, - binding_mode = "released", - binding = ["l", "g", "i"] - ) - self.emit_to("source_views", event) + self._manage_signals("register_command") - event = Event_Factory.create_event( - "register_provider", - provider_name = "LSP Completer", - provider = lsp_manager.provider, - language_ids = [] - ) - self.emit_to("completion", event) + self._manage_provider("register_provider") event = Event_Factory.create_event( "create_source_view", @@ -59,8 +48,8 @@ class Plugin(PluginCode): ) self.emit_to("source_views", event) - source_view = event.response - lsp_manager.lsp_manager_ui.set_source_view(source_view) + scrolled_win, source_view = event.response + lsp_manager.ui_manager.set_source_view(scrolled_win, source_view) def unload(self): Event_Factory.unregister_events( lsp_events.__dict__.items() ) @@ -69,56 +58,61 @@ class Plugin(PluginCode): window = self.request_ui_element("main-window") - lsp_manager.lsp_manager_ui.unmap_parent_resize_event(window) + lsp_manager.ui_manager.unmap_parent_resize_event(window) - event = Event_Factory.create_event("unregister_command", - command_name = "LSP Manager", - command = Handler, + self._manage_signals("unregister_command") + + self._manage_provider("unregister_provider") + + lsp_manager.handle_destroy() + + def _manage_signals(self, action: str): + _commands = Commands + _commands.lsp_manager = lsp_manager + + event = Event_Factory.create_event(action, + command_name = "lsp_manager_toggle", + command = _commands.lsp_manager_toggle, binding_mode = "released", - binding = ["l", "g", "i"] + binding = "l" ) self.emit_to("source_views", event) - event = Event_Factory.create_event( - "unregister_provider", - provider_name = "LSP Completer" + event = Event_Factory.create_event(action, + command_name = "lsp_references", + command = _commands.lsp_references, + binding_mode = "released", + binding = "i" + ) + self.emit_to("source_views", event) + + event = Event_Factory.create_event(action, + command_name = "lsp_implementation", + command = _commands.lsp_implementation, + binding_mode = "released", + binding = "i" + ) + self.emit_to("source_views", event) + + event = Event_Factory.create_event(action, + command_name = "lsp_definition", + command = _commands.lsp_definition, + binding_mode = "released", + binding = "g" + ) + self.emit_to("source_views", event) + + def _manage_provider(self, action: str): + event = Event_Factory.create_event( + action, + provider_name = "LSP Completer", + provider = lsp_manager.provider, + language_ids = [] ) self.emit_to("completion", event) - lsp_manager.handle_destroy() - def run(self): ... def generate_plugin_element(self): ... - - -class Handler: - @staticmethod - def execute( - view: any, - *args, - **kwargs - ): - logger.debug("Command: LSP Manager") - - char_str = args[0] - if char_str in ["g", "i"]: - file = view.command.exec("get_current_file") - buffer = view.get_buffer() - iter = buffer.get_iter_at_mark( buffer.get_insert() ) - line = iter.get_line() - column = iter.get_line_offset() - - if char_str == "g": - lsp_manager.lsp_manager_client.process_goto_definition( - file.ftype, file.fpath, line, column - ) - - return - - if char_str == "i": - return - - lsp_manager.lsp_manager_ui.hide() if lsp_manager.lsp_manager_ui.is_visible() else lsp_manager.lsp_manager_ui.show() diff --git a/plugins/code/language_server_clients/lsp_manager/provider/provider.py b/plugins/code/language_server_clients/lsp_manager/provider/provider.py index a41e0e6..eb96001 100644 --- a/plugins/code/language_server_clients/lsp_manager/provider/provider.py +++ b/plugins/code/language_server_clients/lsp_manager/provider/provider.py @@ -32,20 +32,7 @@ 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 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()): - return False buffer = iter.get_buffer() if buffer.get_context_classes_at_iter(iter) != ['no-spell-check']: @@ -68,6 +55,7 @@ class Provider(GObject.GObject, GtkSource.CompletionProvider): # return GtkSource.CompletionActivation.NONE return GtkSource.CompletionActivation.USER_REQUESTED # return GtkSource.CompletionActivation.INTERACTIVE +# return GtkSource.CompletionActivation.USER_REQUESTED | GtkSource.CompletionActivation.INTERACTIVE def do_populate(self, context): results = self.response_cache.filter_with_context(context) diff --git a/plugins/code/language_server_clients/lsp_manager/provider/provider_response_cache.py b/plugins/code/language_server_clients/lsp_manager/provider/provider_response_cache.py index 2686d91..638439f 100644 --- a/plugins/code/language_server_clients/lsp_manager/provider/provider_response_cache.py +++ b/plugins/code/language_server_clients/lsp_manager/provider/provider_response_cache.py @@ -33,9 +33,13 @@ class ProviderResponseCache(ProviderResponseCacheBase): if self.lsp_manager_client: self.lsp_manager_client.process_file_save(event) - def process_file_change(self, event): + def process_file_text_inserted(self, event): if self.lsp_manager_client: - self.lsp_manager_client.process_file_change(event) + self.lsp_manager_client.process_file_text_inserted(event) + + def process_file_delete_range(self, event): + if self.lsp_manager_client: + self.lsp_manager_client.process_file_delete_range(event) def filter(self, word: str) -> list[dict]: return [] diff --git a/plugins/code/language_server_clients/lsp_manager/response_handlers/default.py b/plugins/code/language_server_clients/lsp_manager/response_handlers/default.py index f936310..3e7f5ad 100644 --- a/plugins/code/language_server_clients/lsp_manager/response_handlers/default.py +++ b/plugins/code/language_server_clients/lsp_manager/response_handlers/default.py @@ -21,6 +21,10 @@ class DefaultHandler(BaseHandler): self._handle_completion(response) case "textDocument/definition": self._handle_definition(response, controller) + case "textDocument/references": + ... + case "textDocument/implementation": + ... case "textDocument/publishDiagnostics": self._handle_diagnostics(response) @@ -130,5 +134,5 @@ class DefaultHandler(BaseHandler): view = view, provider = self.context._provider ) - self.emit_to("completion", event) + self.emit_to("completion", event) diff --git a/plugins/code/language_server_clients/lsp_manager/response_handlers/response_registry.py b/plugins/code/language_server_clients/lsp_manager/response_handlers/response_registry.py index 09403fa..07abe88 100644 --- a/plugins/code/language_server_clients/lsp_manager/response_handlers/response_registry.py +++ b/plugins/code/language_server_clients/lsp_manager/response_handlers/response_registry.py @@ -40,7 +40,7 @@ class ResponseRegistry: 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) diff --git a/plugins/code/language_server_clients/lsp_manager/ui_manager.py b/plugins/code/language_server_clients/lsp_manager/ui_manager.py new file mode 100644 index 0000000..85bc746 --- /dev/null +++ b/plugins/code/language_server_clients/lsp_manager/ui_manager.py @@ -0,0 +1,44 @@ +# Python imports +from os import path + +# Lib imports +import gi + +from gi.repository import GObject +from gi.repository import Gtk + +# Application imports +from .mixins.ui_manager_setup_mixin import UIManagerSetupMixin +from .mixins.ui_manager_events_mixin import UIManagerEventsMixin +from .mixins.ui_manager_clients_mixin import UIManagerClientsMixin + + + +class UIManager( + Gtk.Dialog, + UIManagerSetupMixin, + UIManagerEventsMixin, + UIManagerClientsMixin +): + __gsignals__ = { + 'create-client': (GObject.SignalFlags.RUN_LAST, None, (str, str)), + 'close-client': (GObject.SignalFlags.RUN_LAST, None, (str,)), + } + + def __init__(self): + super(UIManager, self).__init__() + self._USER_HOME = path.expanduser("~") + self.client_configs: dict[str, str] = {} + self.source_view = None + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + + + def map_parent_resize_event(self, parent): + self.size_allocate_id = parent.connect("size-allocate", lambda w, r: self._map_resize(self, parent)) + + def unmap_parent_resize_event(self, parent): + parent.disconnect(self.size_allocate_id) diff --git a/plugins/code/language_server_clients/python_lsp_client/config/lsp-server-config.json b/plugins/code/language_server_clients/python_lsp_client/config/lsp-server-config.json index 5b81dc4..d59954f 100644 --- a/plugins/code/language_server_clients/python_lsp_client/config/lsp-server-config.json +++ b/plugins/code/language_server_clients/python_lsp_client/config/lsp-server-config.json @@ -47,9 +47,16 @@ }, "jedi_completion": { "enabled": true, + "fuzzy": true, + "include_params": false, "include_class_objects": true, - "include_function_objects": true, - "fuzzy": false + "include_function_objects": true + }, + "jedi_signature_help": { + "enabled": true + }, + "jedi_references": { + "enabled": true }, "jedi": { "root_dir": "file://{workspace.folder}", diff --git a/src/core/widgets/code/completion_providers/provider_response_cache_base.py b/src/core/widgets/code/completion_providers/provider_response_cache_base.py index fe9e10e..3166477 100644 --- a/src/core/widgets/code/completion_providers/provider_response_cache_base.py +++ b/src/core/widgets/code/completion_providers/provider_response_cache_base.py @@ -36,8 +36,11 @@ class ProviderResponseCacheBase: def process_file_save(self, event: Code_Event_Types.SavedFileEvent): raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_save' not implemented...") - def process_file_change(self, event: Code_Event_Types.TextChangedEvent): - raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_change' not implemented...") + def process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent): + raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_text_inserted' not implemented...") + + def process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent): + raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_delete_range' not implemented...") def filter(self, word: str) -> list[dict]: raise ProviderResponseCacheException("ProviderResponseCacheBase 'filter' not implemented...") diff --git a/src/core/widgets/code/controllers/completion_controller.py b/src/core/widgets/code/controllers/completion_controller.py index a8fd9ed..f714511 100644 --- a/src/core/widgets/code/controllers/completion_controller.py +++ b/src/core/widgets/code/controllers/completion_controller.py @@ -41,8 +41,10 @@ class CompletionController(ControllerBase): self.provider_process_file_close(event) elif isinstance(event, Code_Event_Types.SavedFileEvent): self.provider_process_file_save(event) - elif isinstance(event, Code_Event_Types.TextChangedEvent): - self.provider_process_file_change(event) + elif isinstance(event, Code_Event_Types.TextInsertedEvent): + self.provider_process_file_text_inserted(event) + elif isinstance(event, Code_Event_Types.DeleteRangeEvent): + self.provider_process_file_delete_range(event) elif isinstance(event, Code_Event_Types.RequestCompletionEvent): self.request_unbound_completion(event) @@ -88,9 +90,13 @@ class CompletionController(ControllerBase): for provider in self._providers.values(): provider.response_cache.process_file_save(event) - def provider_process_file_change(self, event: Code_Event_Types.TextChangedEvent): + def provider_process_file_text_inserted(self, event: Code_Event_Types.TextInsertedEvent): for provider in self._providers.values(): - provider.response_cache.process_file_change(event) + provider.response_cache.process_file_text_inserted(event) + + def provider_process_file_delete_range(self, event: Code_Event_Types.DeleteRangeEvent): + for provider in self._providers.values(): + provider.response_cache.process_file_delete_range(event) def request_unbound_completion(self, event: Code_Event_Types.RequestCompletionEvent): completer = event.view.get_completion() diff --git a/src/core/widgets/code/controllers/views/source_views_controller.py b/src/core/widgets/code/controllers/views/source_views_controller.py index 0e66c8e..5147333 100644 --- a/src/core/widgets/code/controllers/views/source_views_controller.py +++ b/src/core/widgets/code/controllers/views/source_views_controller.py @@ -128,6 +128,7 @@ class SourceViewsController(ControllerBase, list): self.append(source_view) scrolled_win.add(source_view) + scrolled_win.show_all() event = Event_Factory.create_event( "created_source_view", view = source_view diff --git a/src/core/widgets/code/source_buffer.py b/src/core/widgets/code/source_buffer.py index d89462c..4f2883c 100644 --- a/src/core/widgets/code/source_buffer.py +++ b/src/core/widgets/code/source_buffer.py @@ -31,6 +31,7 @@ class SourceBuffer(GtkSource.Buffer): _insert_text, _after_insert_text, _modified_changed, + _delete_range, ): self._handler_ids = [ @@ -39,7 +40,8 @@ class SourceBuffer(GtkSource.Buffer): self.connect("mark-set", _mark_set), self.connect("insert-text", _insert_text), self.connect_after("insert-text", _after_insert_text), - self.connect("modified-changed", _modified_changed) + self.connect("modified-changed", _modified_changed), + self.connect("delete-range", _delete_range) ] def block_changed_signal(self): @@ -54,6 +56,9 @@ class SourceBuffer(GtkSource.Buffer): def block_modified_changed_signal(self): self.handler_block(self._handler_ids[5]) + def block_delete_range(self): + self.handler_block(self._handler_ids[6]) + def unblock_changed_signal(self): self.handler_unblock(self._handler_ids[0]) @@ -66,6 +71,9 @@ class SourceBuffer(GtkSource.Buffer): def unblock_modified_changed_signal(self): self.handler_unblock(self._handler_ids[5]) + def unblock_delete_range(self): + self.handler_block(self._handler_ids[6]) + def clear_signals(self): for handle_id in self._handler_ids: self.disconnect(handle_id) diff --git a/src/core/widgets/code/source_file.py b/src/core/widgets/code/source_file.py index 038cecc..4dd129e 100644 --- a/src/core/widgets/code/source_file.py +++ b/src/core/widgets/code/source_file.py @@ -40,7 +40,8 @@ class SourceFile(GtkSource.File): self._mark_set, self._insert_text, self._after_insert_text, - self._modified_changed + self._modified_changed, + self._delete_range ) def _changed(self, buffer: SourceBuffer): @@ -99,7 +100,19 @@ class SourceFile(GtkSource.File): def _modified_changed(self, buffer: SourceBuffer): event = Event_Factory.create_event( "modified_changed", - file = self, buffer = buffer + file = self, + buffer = buffer + ) + + self.emit(event) + + def _delete_range(self, buffer: SourceBuffer, start: Gtk.TextIter, end: Gtk.TextIter): + event = Event_Factory.create_event( + "delete_range", + file = self, + buffer = buffer, + start = start, + end = end, ) self.emit(event) diff --git a/src/libs/dto/code/events/__init__.py b/src/libs/dto/code/events/__init__.py index 5c380c1..77cc360 100644 --- a/src/libs/dto/code/events/__init__.py +++ b/src/libs/dto/code/events/__init__.py @@ -26,6 +26,7 @@ from .get_source_views_event import GetSourceViewsEvent from .create_command_system_event import CreateCommandSystemEvent from .request_completion_event import RequestCompletionEvent from .cursor_moved_event import CursorMovedEvent +from .delete_range_event import DeleteRangeEvent from .modified_changed_event import ModifiedChangedEvent from .text_changed_event import TextChangedEvent from .text_inserted_event import TextInsertedEvent diff --git a/src/libs/dto/code/events/delete_range_event.py b/src/libs/dto/code/events/delete_range_event.py new file mode 100644 index 0000000..60318d1 --- /dev/null +++ b/src/libs/dto/code/events/delete_range_event.py @@ -0,0 +1,19 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +# Application imports +from .code_event import CodeEvent + + + +@dataclass +class DeleteRangeEvent(CodeEvent): + start: Gtk.TextIter = None + end: Gtk.TextIter = None diff --git a/src/libs/dto/code/events/unregister_provider_event.py b/src/libs/dto/code/events/unregister_provider_event.py index 7f8e9cf..2372b7f 100644 --- a/src/libs/dto/code/events/unregister_provider_event.py +++ b/src/libs/dto/code/events/unregister_provider_event.py @@ -1,7 +1,11 @@ # Python imports -from dataclasses import dataclass +from dataclasses import dataclass, field # Lib imports +import gi +gi.require_version('GtkSource', '4') + +from gi.repository import GtkSource # Application imports from .code_event import CodeEvent @@ -10,4 +14,6 @@ from .code_event import CodeEvent @dataclass class UnregisterProviderEvent(CodeEvent): - provider_name: str = "" + provider_name: str = "" + provider: GtkSource.CompletionProvider = None + language_ids: list = field(default_factory=lambda: [])