From 2d4c8e4f311e1f7b468dde2a416f5b6fc1f2eff9 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Mon, 13 Apr 2026 00:54:51 -0500 Subject: [PATCH] feat(lsp): improve shutdown lifecycle and response handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add explicit shutdown and exit requests to LSP client - Rename initialized message to initialized notification - Handle initialize → send initialized notification - Handle shutdown → send exit notification fix(lsp): delay client teardown to allow graceful shutdown - Close LSP client and response handlers after timeout (GLib) feat(vte): major terminal widget improvements - Switch to spawn_async with full environment inheritance - Improve PROMPT_COMMAND handling for cwd tracking (OSC7) - Enable scrollback, disable audible bell, enable scroll-on-output - Add clipboard shortcuts (Ctrl+Shift+C/V), middle-click paste - Track current directory changes and update UI label - Add proper signal wiring and cleanup on destroy feat(ui): improve Plugins dialog behavior - Make dialog non-modal and centered on parent window - Auto-hide on focus loss and Ctrl+Shift+P shortcut - Ensure proper destroy-with-parent behavior refactor: add type hints and minor cleanup - Add explicit typing for VteWidget fields - Improve signal naming consistency - Minor formatting and cleanup in folding and LSP modules --- .../lsp_manager/client/lsp_client_events.py | 10 +- .../lsp_manager/dto/code/lsp/lsp_messages.py | 12 ++- .../lsp_manager/lsp_manager.py | 15 ++- .../lsp_manager/response_handlers/default.py | 4 + src/core/widgets/vte_widget.py | 98 +++++++++++++++---- src/plugins/plugins_ui.py | 23 ++++- 6 files changed, 138 insertions(+), 24 deletions(-) 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 8b7dd3e..b262e67 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 @@ -15,6 +15,8 @@ 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 +from ..dto.code.lsp.lsp_messages import shutdown_request +from ..dto.code.lsp.lsp_messages import exit_request @@ -36,9 +38,15 @@ class LSPClientEvents: self._init_params["initializationOptions"] = self._init_opts self.send_request("initialize", self._init_params) - def send_initialized_message(self): + def send_initialized_notification(self): self.send_notification("initialized") + def send_shutdown_request(self): + self.send_request("shutdown") + + def send_exit_notification(self): + self.send_notification("exit") + def _lsp_did_open(self, data: dict): method = "textDocument/didOpen" params = didopen_notification["params"] 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 ec28b03..14ab89c 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 @@ -183,7 +183,6 @@ references_request = { } } - symbols_request = { "method": "textDocument/documentSymbol", "params": { @@ -195,3 +194,14 @@ symbols_request = { } } } + +shutdown_request = { + "method": "shutdown", + "params": None +} + +exit_request = { + "method": "exit", + "params": None +} + 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 eb0b8e1..fda6487 100644 --- a/plugins/code/language_server_clients/lsp_manager/lsp_manager.py +++ b/plugins/code/language_server_clients/lsp_manager/lsp_manager.py @@ -1,6 +1,9 @@ # Python imports # Lib imports +import gi + +from gi.repository import GLib # Application imports from libs.controllers.controller_base import ControllerBase @@ -109,8 +112,16 @@ class LSPManager(ControllerBase): return True def close_client(self, lang_id: str) -> bool: - self.client_manager.close_client(lang_id) - self.response_registry.close_handler(lang_id) + controller = self.client_manager.get_active_client() + controller.send_shutdown_request() + + def _close(): + self.client_manager.close_client(lang_id) + self.response_registry.close_handler(lang_id) + + return False + + GLib.timeout_add(5000, _close) return True 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 3e7f5ad..e8c8481 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 @@ -17,6 +17,10 @@ class DefaultHandler(BaseHandler): def handle(self, method: str, response, controller): match method: + case "initialize": + controller.send_initialized_notification() + case "shutdown": + controller.send_exit_notification() case "textDocument/completion": self._handle_completion(response) case "textDocument/definition": diff --git a/src/core/widgets/vte_widget.py b/src/core/widgets/vte_widget.py index 3e931d4..888c75d 100644 --- a/src/core/widgets/vte_widget.py +++ b/src/core/widgets/vte_widget.py @@ -28,8 +28,8 @@ class VteWidget(Vte.Terminal): def __init__(self): super(VteWidget, self).__init__() - self.cd_cmd_prefix = ("cd".encode(), "cd ".encode()) - self.dont_process = False + self.cd_cmd_prefix: tuple = ("cd".encode(), "cd ".encode()) + self.dont_process: bool = False self._setup_styling() self._setup_signals() @@ -48,9 +48,17 @@ class VteWidget(Vte.Terminal): self.set_hexpand(True) self.set_enable_sixel(True) self.set_cursor_shape( Vte.CursorShape.IBEAM ) + self.set_audible_bell(False) + self.set_scroll_on_output(True) def _setup_signals(self): - self.connect("commit", self._commit) + self.connect("commit", self._handle_commit) + self.connect("current-directory-uri-changed", self._handle_path_change) + self.connect("selection-changed", self._handle_selection) + self.connect("button-press-event", self._on_button_press) + self.connect("key-press-event", self._on_key_press) + self.connect("key-release-event", self._on_key_release) + self.connect("destroy", self._handle_destroy) def _subscribe_to_events(self): event_system.subscribe("update_term_path", self.update_term_path) @@ -59,35 +67,87 @@ class VteWidget(Vte.Terminal): ... def _do_session_spawn(self): - env = [ - "DISPLAY=:0", - "LC_ALL=C", - "TERM='xterm-256color'", - f"HOME='{settings_manager.path_manager.get_home_path()}'", - "XDG_RUNTIME_DIR='/run/user/1000'", - f"XAUTHORITY='{settings_manager.path_manager.get_home_path()}/.Xauthority'", - "HISTFILE=/dev/null", - "HISTSIZE=0", - "HISTFILESIZE=0", - "PS1=\\h@\\u \\W -->: ", - ] + env_dict = os.environ.copy() + existing_pc = env_dict.get("PROMPT_COMMAND", "") + # Note: Needed for 'current-directory-uri-changed' to work. + # Make sure user .bashrc doesn't affect it... + osc7 = 'printf "\\033]7;file://%s%s\\007" "$PWD"' - self.spawn_sync( + env_dict.update({ + "LC_ALL": "C", + "TERM": "xterm-256color", + "HISTFILE": "/dev/null", + "HISTSIZE": "0", + "HISTFILESIZE": "0", + "PS1": "\\h@\\u \\W -->: ", + "PROMPT_COMMAND": f"{osc7};{existing_pc}" if existing_pc else osc7, + }) + + env = [f"{k}={v}" for k, v in env_dict.items()] + + self.spawn_async( Vte.PtyFlags.DEFAULT, settings_manager.path_manager.get_home_path(), ["/bin/bash"], env, GLib.SpawnFlags.DEFAULT, - None, None, + None, None, -1, None, None, ) startup_cmds = [ ] + self.set_scrollback_lines(15000) for i in startup_cmds: self.run_command(i) - def _commit(self, terminal, text, size): + def _handle_destroy(self, terminal): + logger.debug("Destroying terminal...") + terminal.disconnect_by_func(terminal._handle_commit) + terminal.disconnect_by_func(terminal._handle_path_change) + terminal.disconnect_by_func(terminal._handle_selection) + terminal.disconnect_by_func(terminal._on_button_press) + terminal.disconnect_by_func(terminal._on_key_press) + terminal.disconnect_by_func(terminal._on_key_release) + terminal.disconnect_by_func(terminal._handle_destroy) + + def _handle_path_change(self, terminal): + if not hasattr(self, "label"): return + + uri = terminal.get_current_directory_uri().replace("file://", "") + + terminal.label.set_text(uri) + terminal.label.set_tooltip_text(uri) + + def _handle_selection(self, *args): + if self.get_has_selection(): + self.copy_primary() + + def _on_button_press(self, widget, event): + if event.button == 2: # middle click + self.paste_clipboard() + return True + + def _on_key_press(self, widget, event): + ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK + shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK + + if ctrl_pressed: + if shift_pressed: + if event.keyval in [Gdk.KEY_C, Gdk.KEY_V]: + if event.keyval == Gdk.KEY_C: + self.copy_clipboard() + elif event.keyval == Gdk.KEY_V: + self.paste_clipboard() + + return True + + return False + + def _on_key_release(self, widget, event): + ... + + def _handle_commit(self, terminal, text, size): if self.dont_process: self.dont_process = False return @@ -127,4 +187,4 @@ class VteWidget(Vte.Terminal): self.run_command(cmd) def run_command(self, cmd: str): - self.feed_child_binary(bytes(cmd, 'utf8')) \ No newline at end of file + self.feed_child_binary(bytes(cmd, 'utf8')) diff --git a/src/plugins/plugins_ui.py b/src/plugins/plugins_ui.py index 60e729f..17649e4 100644 --- a/src/plugins/plugins_ui.py +++ b/src/plugins/plugins_ui.py @@ -2,7 +2,10 @@ # Lib imports import gi +gi.require_version('Gdk', '3.0') from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GLib # Application imports @@ -25,6 +28,7 @@ class PluginsUI(Gtk.Dialog): self.set_title("Plugins") self.set_size_request(450, 530) + self.set_modal(False) self.set_deletable(False) self.set_skip_pager_hint(True) self.set_skip_taskbar_hint(True) @@ -35,9 +39,13 @@ class PluginsUI(Gtk.Dialog): window = widget_registery.get_object("main-window") self.set_transient_for(window) + self.set_destroy_with_parent(True) + + self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) def _setup_signals(self): - ... + self.connect("focus-out-event", self._on_focus_out) + self.connect("key-release-event", self._on_key_release) def _subscribe_to_events(self): ... @@ -59,6 +67,19 @@ class PluginsUI(Gtk.Dialog): scrolled_win.show_all() + def _on_key_release(self, widget, event): + ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK + shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK + + if ctrl_pressed: + if shift_pressed: + if event.keyval == Gdk.KEY_P: + self.hide() + + def _on_focus_out(self, *args): + self.hide() + GLib.idle_add(self.hide) + def add_row(self, manifest_meta, callback: callable): box = Gtk.Box() plugin_lbl = Gtk.Label(label = manifest_meta.manifest.name)