diff --git a/TODO.md b/TODO.md index 9635647..c1ac118 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,5 @@ ___ ### Add -1. Add Godot LSP Client -1. Add Terminal plugin 1. Add i to **lsp_manager** to list who implements xyz ___ @@ -10,8 +8,8 @@ ___ ___ ### Fix +- Fix LSP WS Server to Godot LSP Server communication - Fix z in multi-insert mode being funky. Insure updates happen on block level. I.E, maybe push updates to queue to insure block undo/redo? -- Fix on lsp client unload to close files lsp side and unload server endpoint ___ 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/plugins/code/ui/code_fold/fold_types.py b/plugins/code/ui/code_fold/fold_types.py index 946a128..824451f 100644 --- a/plugins/code/ui/code_fold/fold_types.py +++ b/plugins/code/ui/code_fold/fold_types.py @@ -25,6 +25,28 @@ FOLD_NODES = { "with_statement", "try_statement", }, + "javascript": { + "function_declaration", + "class_declaration", + "if_statement", + "for_statement", + "while_statement", + "switch_statement", + "try_statement", + }, + "html": { + "element", + "attribute", + }, + "css": { + "rule_set", + "selector", + "declaration", + }, + "json": { + "object", + "array", + }, "java": { "class_declaration", "method_declaration", @@ -35,8 +57,30 @@ FOLD_NODES = { "switch_expression", "block", }, - "json": { - "object", - "array", + "c": { + "function_definition", + "struct_definition", + "if_statement", + "for_statement", + "while_statement", + "switch_statement", }, -} \ No newline at end of file + "cpp": { + "function_definition", + "class_definition", + "struct_definition", + "namespace_definition", + "if_statement", + "for_statement", + "while_statement", + "switch_statement", + }, + "go": { + "function_declaration", + "type_declaration", + "if_statement", + "for_statement", + "select_statement", + "switch_statement", + }, +} diff --git a/plugins/code/ui/code_fold/plugin.py b/plugins/code/ui/code_fold/plugin.py index 82da316..81e3722 100644 --- a/plugins/code/ui/code_fold/plugin.py +++ b/plugins/code/ui/code_fold/plugin.py @@ -25,15 +25,19 @@ class Plugin(PluginCode): self.view = event.view event = Event_Factory.create_event( - "get_file", buffer=self.view.get_buffer() + "get_file", buffer = self.view.get_buffer() ) self.emit_to("files", event) file = event.response if not file: return - if file.ftype not in FOLD_NODES: return - if not hasattr(file, "ast"): return + if file.ftype not in FOLD_NODES: + self.view.fold_start_set = {} + return + if not hasattr(file, "ast"): + self.view.fold_start_set = {} + return buffer = file.buffer if not buffer.get_tag_table().lookup("invisible"): diff --git a/plugins/code/ui/terminals/__init__.py b/plugins/code/ui/terminals/__init__.py new file mode 100644 index 0000000..d36fa8c --- /dev/null +++ b/plugins/code/ui/terminals/__init__.py @@ -0,0 +1,3 @@ +""" + Pligin Module +""" diff --git a/plugins/code/ui/terminals/__main__.py b/plugins/code/ui/terminals/__main__.py new file mode 100644 index 0000000..a576329 --- /dev/null +++ b/plugins/code/ui/terminals/__main__.py @@ -0,0 +1,3 @@ +""" + Pligin Package +""" diff --git a/plugins/code/ui/terminals/manifest.json b/plugins/code/ui/terminals/manifest.json new file mode 100644 index 0000000..cc8fe0b --- /dev/null +++ b/plugins/code/ui/terminals/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "Terminals", + "author": "ITDominator", + "version": "0.0.1", + "support": "", + "requests": {} +} diff --git a/plugins/code/ui/terminals/plugin.py b/plugins/code/ui/terminals/plugin.py new file mode 100644 index 0000000..cc2b4b4 --- /dev/null +++ b/plugins/code/ui/terminals/plugin.py @@ -0,0 +1,60 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from plugins.plugin_types import PluginCode + +from .terminals_view import TerminalsView + + + +terminals_view = TerminalsView() + + + +class Plugin(PluginCode): + def __init__(self): + super(Plugin, self).__init__() + + + def _controller_message(self, event: Code_Event_Types.CodeEvent): + ... + + def load(self): + footer = self.request_ui_element("footer-container") + footer.add( terminals_view ) + + self._manage_signals("register_command") + + def unload(self): + self._manage_signals("unregister_command") + terminals_view.destroy() + + def _manage_signals(self, action: str): + event = Event_Factory.create_event(action, + command_name = "terminals", + command = Handler, + binding_mode = "released", + binding = "." + ) + + self.emit_to("source_views", event) + + def run(self): + ... + + +class Handler: + @staticmethod + def execute( + view: any, + *args, + **kwargs + ): + logger.debug("Command: Terminal") + terminals_view.set_code_view(view) + + terminals_view.hide() if terminals_view.is_visible() else terminals_view.show() diff --git a/plugins/code/ui/terminals/terminals_view.py b/plugins/code/ui/terminals/terminals_view.py new file mode 100644 index 0000000..1754f7f --- /dev/null +++ b/plugins/code/ui/terminals/terminals_view.py @@ -0,0 +1,142 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') + +from gi.repository import Gtk +from gi.repository import GLib +from gi.repository import Gdk +from gi.repository import Pango + +# Application imports +from .vte_widget import VteWidget + + + +class TerminalsView(Gtk.Notebook): + def __init__(self): + super(TerminalsView, self).__init__() + + self.code_view = None + + self._setup_styling() + self._setup_signals() + self._load_widgets() + + self.show_all() + self.hide() + + + def _setup_styling(self): + ctx = self.get_style_context() + ctx.add_class("terminals-view") + + self.set_scrollable(True) + + def _setup_signals(self): + self.connect("show", self._handle_show) + self.connect("hide", self._handle_hide) + self.connect("destroy", self._handle_destroy) + + def _load_widgets(self): + hbox = Gtk.Box() + self.add_bttn = Gtk.Button(label = "✛") + self.hide_bttn = Gtk.Button(label = "-") + self.add_bttn.connect("clicked", self._create_terminal) + self.hide_bttn.connect("clicked", self._hide_view) + + hbox.add(self.add_bttn) + hbox.add(self.hide_bttn) + self.set_action_widget(hbox, Gtk.PackType.END) + + self.create_terminal() + hbox.show_all() + + def _generate_terminal_parts(self): + label = Gtk.Label(label = "...") + vte_widget = VteWidget() + + vte_widget.hide_view = self.hide + vte_widget.create_terminal = self.create_terminal + vte_widget.close_terminal = self.close_terminal + vte_widget.prev_terminal = self.prev_terminal + vte_widget.next_terminal = self.next_terminal + + label.set_text( vte_widget.get_home_path() ) + label.set_tooltip_text( vte_widget.get_home_path() ) + label.set_ellipsize(Pango.EllipsizeMode.START) + label.set_single_line_mode(True) + label.set_max_width_chars(32) + label.set_size_request(240, -1) + + vte_widget.bind_label(label) + + return label, vte_widget + + def _handle_show(self, widget): + i = widget.get_current_page() + term = widget.get_nth_page(i) + + GLib.idle_add(term.grab_focus) + + def _handle_hide(self, widget): + if not self.code_view: return + GLib.idle_add(self.code_view.grab_focus) + + def _hide_view(self, widget): + self.hide() + + def _handle_destroy(self, widget): + widget.disconnect_by_func(widget._handle_show) + widget.disconnect_by_func(widget._handle_hide) + widget.disconnect_by_func(widget._handle_destroy) + self.add_bttn.disconnect_by_func(self._create_terminal) + self.hide_bttn.disconnect_by_func(self._hide_view) + + def _create_terminal(self, widget): + self.create_terminal() + + def set_code_view(self, widget): + self.code_view = widget + + def create_terminal(self): + label, vte_widget = self._generate_terminal_parts() + index = self.append_page(vte_widget, label) + self.set_tab_detachable(vte_widget, True) + self.set_tab_reorderable(vte_widget, True) + + self.set_current_page(index) + GLib.idle_add(vte_widget.grab_focus) + + self.show_all() + + def close_terminal(self): + size = self.get_n_pages() + if size == 1: return + + i = self.get_current_page() + widget = self.get_nth_page(i) + self.remove_page(i) + widget.destroy() + + def prev_terminal(self): + i = self.get_current_page() - 1 + size = self.get_n_pages() + + if i < 0: + self.set_current_page(size - 1) + return + + self.prev_page() + + def next_terminal(self): + i = self.get_current_page() + 1 + size = self.get_n_pages() + + if i == size: + self.set_current_page(0) + return + + self.next_page() diff --git a/plugins/code/ui/terminals/vte_widget.py b/plugins/code/ui/terminals/vte_widget.py new file mode 100644 index 0000000..d69ad26 --- /dev/null +++ b/plugins/code/ui/terminals/vte_widget.py @@ -0,0 +1,176 @@ +# Python imports +import os +from os import path + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +gi.require_version('Vte', '2.91') +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GLib +from gi.repository import Vte + +# Application imports + + + +class VteWidgetException(Exception): + ... + + + +class VteWidget(Vte.Terminal): + """ + https://stackoverflow.com/questions/60454326/how-to-implement-a-linux-terminal-in-a-pygtk-app-like-vscode-and-pycharm-has + """ + + def __init__(self): + super(VteWidget, self).__init__() + + self._USER_HOME: str = path.expanduser('~') + + self._setup_styling() + self._setup_signals() + self._subscribe_to_events() + self._load_widgets() + self._do_session_spawn() + + self.show() + + + def _setup_styling(self): + ctx = self.get_style_context() + ctx.add_class("vte-widget") + + self.set_clear_background(False) + 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._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): + ... + + def _load_widgets(self): + ... + + def _do_session_spawn(self): + 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"' + + 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, + self._USER_HOME, + ["/bin/bash"], + env, + GLib.SpawnFlags.DEFAULT, + None, None, -1, None, None, + ) + + self.set_scrollback_lines(15000) + + 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_commit(self, terminal, text, size): + ... + + 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 + + if event.keyval in [ + Gdk.KEY_period, Gdk.KEY_t, Gdk.KEY_w, Gdk.KEY_Up, Gdk.KEY_Down + ]: + if event.keyval == Gdk.KEY_period: + if hasattr(self, "hide_view"): + GLib.timeout_add(200, self.hide_view) + elif event.keyval == Gdk.KEY_t: + if hasattr(self, "create_terminal"): + self.create_terminal() + elif event.keyval == Gdk.KEY_w: + if hasattr(self, "close_terminal"): + self.close_terminal() + elif event.keyval == Gdk.KEY_Up: + if hasattr(self, "prev_terminal"): + self.prev_terminal() + elif event.keyval == Gdk.KEY_Down: + if hasattr(self, "next_terminal"): + self.next_terminal() + + return True + + return False + + def _on_key_release(self, widget, event): + ... + + 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 get_home_path(self): + return self._USER_HOME + + def bind_label(self, label: Gtk.Label): + self.label = label + + def run_command(self, cmd: str): + self.feed_child_binary(bytes(cmd, 'utf8')) 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)