feat: Complete plugin lifecycle management with lazy loading and runtime reload

Major changes:
- Add unload() method to all plugins for proper cleanup (unregister commands/providers/LSP clients, destroy widgets, clear state)
- Implement lazy widget loading via "show" signal across all containers
- Add autoload: false manifest option for manual/conditional plugin loading
- Add Plugins UI with runtime load/unload toggle via Ctrl+Shift+p
- Implement controller unregistration system with proper signal disconnection
- Add new events: UnregisterCommandEvent, GetFilesEvent, GetSourceViewsEvent, TogglePluginsUiEvent
- Fix signal leaks by tracking and disconnecting handlers in widgets (search/replace, LSP manager, tabs, telescope, markdown preview)
- Add Save/Save As to tabs context menu
- Improve search/replace behavior (selection handling, focus management)
- Add telescope file initialization from existing loaded files
- Refactor plugin reload watcher to dynamically add/remove plugins on filesystem changes
- Add new plugins: file_history, extend_source_view_menu, godot_lsp_client
- Fix bug in prettify_json (undefined variable reference)
This commit is contained in:
2026-03-21 13:22:20 -05:00
parent 21dd86ad3d
commit 77a3b71d31
98 changed files with 1520 additions and 297 deletions

View File

@@ -28,5 +28,12 @@ class Plugin(PluginCode):
editors_container = self.request_ui_element("editors-container")
editors_container.add( code_minimap )
event = Event_Factory.create_event("get_active_view")
self.emit_to("source_views", event)
code_minimap.set_smini_view(event.response)
def unload(self):
code_minimap.destroy()
def run(self):
...

View File

@@ -35,7 +35,6 @@ class InfoBarWidget(Gtk.Box):
def _subscribe_to_events(self):
...
def _load_widgets(self):
self.path_label = Gtk.Label(label = "...")
self.line_char_label = Gtk.Label(label = "1:0")
@@ -92,5 +91,3 @@ class InfoBarWidget(Gtk.Box):
encoding_type = "utf-8" if not encoding_type else encoding_type
self.encoding_label.set_text(encoding_type)

View File

@@ -28,5 +28,8 @@ class Plugin(PluginCode):
header = self.request_ui_element("header-container")
header.add( info_bar_widget )
def unload(self):
info_bar_widget.destroy()
def run(self):
...

View File

@@ -3,5 +3,6 @@
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"autoload": false,
"requests": {}
}

View File

@@ -7,6 +7,7 @@ gi.require_version('Gdk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
# Application imports
from core.widgets.webkit.webkit_ui import WebkitUI
@@ -47,6 +48,7 @@ class MarkdownPreview(Gtk.Popover, MarkdownPreviewMixin):
def _setup_signals(self):
self.connect("hide", self._handle_hide)
self.connect("show", self._handle_show)
self.connect("destroy", self._handle_destroy)
def _load_widgets(self):
box = Gtk.Box()
@@ -71,7 +73,6 @@ class MarkdownPreview(Gtk.Popover, MarkdownPreviewMixin):
box.set_orientation(Gtk.Orientation.VERTICAL)
self.start_stop_bttn.connect("clicked", self._tggle_preview_updates)
settings_bttn.connect("clicked", self._handle_settings)
bttn_box.pack_end(self.start_stop_bttn, expand = False, fill = False, padding = 1)
bttn_box.pack_end(settings_bttn, expand = False, fill = False, padding = 1)
@@ -98,5 +99,8 @@ class MarkdownPreview(Gtk.Popover, MarkdownPreviewMixin):
def _tggle_preview_updates(self, widget):
self.is_preview_paused = not self.is_preview_paused
def _handle_settings(self, widget):
...
def _handle_destroy(self):
self.disconnect_by_func(self._handle_hide)
self.disconnect_by_func(self._handle_show)
self.disconnect_by_func(self._handle_destroy)
self.start_stop_bttn.disconnect_by_func(self._tggle_preview_updates)

View File

@@ -51,6 +51,18 @@ class Plugin(PluginCode):
self.emit_to("source_views", event)
def unload(self):
event = Event_Factory.create_event("unregister_command",
command_name = "tggle_markdown_preview",
command = Handler,
binding_mode = "released",
binding = "<Shift><Control>m"
)
self.emit_to("source_views", event)
markdown_preview.destroy()
def run(self):
...
@@ -67,4 +79,14 @@ class Handler:
if not markdown_preview.can_hide:
markdown_preview.can_hide = True
markdown_preview.popdown() if markdown_preview.is_visible() else markdown_preview.popup()
if markdown_preview.is_visible():
markdown_preview.popdown()
return
file = view.command.exec("get_current_file")
if not file or not file.get_location(): return
buffer = view.get_buffer()
markdown_preview.popup()
markdown_preview.fpath = file.get_location().get_path()
markdown_preview._do_markdown_translate(buffer)

View File

@@ -17,19 +17,17 @@ class SearchReplaceMixin(SearchMixin, ReplaceMixin):
search_text = entry.get_text()
buffer = self.active_view.get_buffer()
if buffer.get_has_selection() and not search_text:
if not self.mode_bttn_box.in_selection:
start_itr, end_itr = buffer.get_selection_bounds()
if not buffer.get_has_selection() and search_text: return
if self.mode_bttn_box.in_selection: return
entry.set_text(
buffer.get_text(
start_itr,
end_itr,
include_hidden_chars = False
)
)
return
start_itr, end_itr = buffer.get_selection_bounds()
entry.set_text(
buffer.get_text(
start_itr,
end_itr,
include_hidden_chars = False
)
)
def _find_entry_search_change(self, entry):
search_text = entry.get_text()

View File

@@ -33,44 +33,44 @@ class ModeButtons(Gtk.ButtonBox):
ctx.add_class("search-replace-mode-buttons")
def _setup_signals(self):
...
self.connect("destroy", self._handle_destroy)
def _load_widgets(self):
use_regex_bttn = Gtk.ToggleButton(label = ".*")
match_case_bttn = Gtk.ToggleButton(label = "Aa")
in_selection_bttn = Gtk.ToggleButton()
whole_word_bttn = Gtk.ToggleButton()
hide_bttn = Gtk.Button(label = "X")
self.use_regex_bttn = Gtk.ToggleButton(label = ".*")
self.match_case_bttn = Gtk.ToggleButton(label = "Aa")
self.in_selection_bttn = Gtk.ToggleButton()
self.whole_word_bttn = Gtk.ToggleButton()
self.hide_bttn = Gtk.Button(label = "X")
use_regex_bttn.set_sensitive(False)
self.use_regex_bttn.set_sensitive(False)
use_regex_bttn.set_tooltip_text("Use Regex")
match_case_bttn.set_tooltip_text("Match Case")
in_selection_bttn.set_tooltip_text("Only In Selection")
whole_word_bttn.set_tooltip_text("Whole Word")
self.use_regex_bttn.set_tooltip_text("Use Regex")
self.match_case_bttn.set_tooltip_text("Match Case")
self.in_selection_bttn.set_tooltip_text("Only In Selection")
self.whole_word_bttn.set_tooltip_text("Whole Word")
use_regex_bttn.connect("toggled", self._toggled_button, "use_regex")
match_case_bttn.connect("toggled", self._toggled_button, "match_case")
in_selection_bttn.connect("toggled", self._toggled_button, "in_selection")
whole_word_bttn.connect("toggled", self._toggled_button, "whole_word")
self.use_regex_bttn.connect("toggled", self._toggled_button, "use_regex")
self.match_case_bttn.connect("toggled", self._toggled_button, "match_case")
self.in_selection_bttn.connect("toggled", self._toggled_button, "in_selection")
self.whole_word_bttn.connect("toggled", self._toggled_button, "whole_word")
hide_bttn.connect(
self.hide_bttn_id = self.hide_bttn.connect(
"clicked",
lambda widget: self.get_parent().hide()
)
in_selection_bttn.set_image(
self.in_selection_bttn.set_image(
Gtk.Image.new_from_file("images/only-in-selection.png")
)
whole_word_bttn.set_image(
self.whole_word_bttn.set_image(
Gtk.Image.new_from_file("images/whole-word.png")
)
self.add(use_regex_bttn)
self.add(match_case_bttn)
self.add(in_selection_bttn)
self.add(whole_word_bttn)
self.add(hide_bttn)
self.add(self.use_regex_bttn)
self.add(self.match_case_bttn)
self.add(self.in_selection_bttn)
self.add(self.whole_word_bttn)
self.add(self.hide_bttn)
def _toggled_button(self, toggle_button, mode: str):
setattr(self, mode, not getattr(self, mode))
@@ -79,4 +79,12 @@ class ModeButtons(Gtk.ButtonBox):
def request_update(self):
raise ModeException("Must by 'monkey' patched from search_replace.py")
def _handle_destroy(self, widget):
self.disconnect_by_func(self._handle_destroy)
self.use_regex_bttn.disconnect_by_func(self._toggled_button)
self.match_case_bttn.disconnect_by_func(self._toggled_button)
self.in_selection_bttn.disconnect_by_func(self._toggled_button)
self.whole_word_bttn.disconnect_by_func(self._toggled_button)
self.hide_bttn.disconnect(self.hide_bttn_id)

View File

@@ -49,6 +49,17 @@ class Plugin(PluginCode):
self.emit_to("source_views", event)
def unload(self):
event = Event_Factory.create_event("unregister_command",
command_name = "search_replace",
command = Handler,
binding_mode = "released",
binding = ["<Control>f", "<Control>r"]
)
self.emit_to("source_views", event)
search_replace.destroy()
def run(self):
...
@@ -63,4 +74,7 @@ class Handler:
logger.debug("Command: Search/Replace")
search_replace.last_key = args[0]
if not search_replace.active_view:
search_replace.active_view = view
search_replace.hide() if search_replace.is_visible() else search_replace.show()

View File

@@ -22,7 +22,7 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin):
self.active_view = None
self.highlight_tag: Gtk.TextTag = None
self.matches: list[tuple] = []
self.last_key: str = None
self.last_key: str = "f"
self._setup_styling()
self._setup_signals()
@@ -48,6 +48,7 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin):
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):
self.status_lbl = Gtk.Label(label = "Find in Current Buffer")
@@ -57,10 +58,10 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin):
self.find_entry = Gtk.SearchEntry()
self.replace_entry = Gtk.SearchEntry()
find_bttn = Gtk.Button(label = "Find")
find_all_bttn = Gtk.Button(label = "Find All")
replace_bttn = Gtk.Button(label = "Replace")
replace_all_bttn = Gtk.Button(label = "Replace All")
self.find_bttn = Gtk.Button(label = "Find")
self.find_all_bttn = Gtk.Button(label = "Find All")
self.replace_bttn = Gtk.Button(label = "Replace")
self.replace_all_bttn = Gtk.Button(label = "Replace All")
self.find_entry.set_hexpand(True)
self.replace_entry.set_hexpand(True)
@@ -81,21 +82,24 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin):
self.replace_entry.connect("key-release-event", self._replace_entry_key_release_event)
self.replace_entry.connect("activate", self._replace_entry_activate)
find_bttn.connect(
self.find_handler_id = self.find_bttn.connect(
"clicked",
lambda button: self._find_entry_next_match(self.find_entry)
)
find_all_bttn.connect(
self.find_all_handler_id = self.find_all_bttn.connect(
"clicked",
lambda button: self._find_entry_search_change(self.find_entry)
)
replace_bttn.connect(
self.replace_handler_id = self.replace_bttn.connect(
"clicked",
lambda button: self._replace_entry_activate(self.replace_entry)
)
replace_all_bttn.connect(
self.replace_all_handler_id = self.replace_all_bttn.connect(
"clicked",
lambda button: self._replace_all_activate(self.replace_entry)
lambda button: self._replace_all_activate(self.replace_entry)
)
self.attach(child = self.status_lbl, left = 0, top = 0, width = 3, height = 1)
@@ -103,12 +107,12 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin):
self.attach(child = self.mode_bttn_box, left = 5, top = 0, width = 2, height = 1)
self.attach(child = self.find_entry, left = 0, top = 1, width = 5, height = 1)
self.attach(child = find_bttn, left = 5, top = 1, width = 1, height = 1)
self.attach(child = find_all_bttn, left = 6, top = 1, width = 1, height = 1)
self.attach(child = self.find_bttn, left = 5, top = 1, width = 1, height = 1)
self.attach(child = self.find_all_bttn, left = 6, top = 1, width = 1, height = 1)
self.attach(child = self.replace_entry, left = 0, top = 2, width = 5, height = 1)
self.attach(child = replace_bttn, left = 5, top = 2, width = 1, height = 1)
self.attach(child = replace_all_bttn, left = 6, top = 2, width = 1, height = 1)
self.attach(child = self.replace_bttn, left = 5, top = 2, width = 1, height = 1)
self.attach(child = self.replace_all_bttn, left = 6, top = 2, width = 1, height = 1)
def _handle_show(self, widget):
@@ -117,9 +121,9 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin):
self.find_entry.grab_focus()
return
self.replace_entry.grab_focus()
# Fake focus call to prompt search
self._find_entry_focus_in_event(self.find_entry, None)
self._find_entry_focus_in_event(self.find_entry, None)
self.replace_entry.grab_focus()
def _handle_hide(self, widget):
if not self.active_view: return
@@ -197,3 +201,22 @@ class SearchReplace(Gtk.Grid, SearchReplaceMixin):
start_itr, end_itr = buffer.get_bounds()
buffer.remove_tag(self.highlight_tag, start_itr, end_itr)
def _handle_destroy(self, widget):
self.disconnect_by_func(self._handle_show)
self.disconnect_by_func(self._handle_hide)
self.disconnect_by_func(self._handle_destroy)
self.find_entry.disconnect_by_func(self._find_entry_focus_in_event)
self.find_entry.disconnect_by_func(self._find_entry_key_release_event)
self.find_entry.disconnect_by_func(self._find_entry_activate)
self.find_entry.disconnect_by_func(self._find_entry_search_change)
self.find_entry.disconnect_by_func(self._find_entry_next_match)
self.find_entry.disconnect_by_func(self._find_entry_previous_match)
self.replace_entry.disconnect_by_func(self._replace_entry_key_release_event)
self.replace_entry.disconnect_by_func(self._replace_entry_activate)
self.find_bttn.disconnect(self.find_handler_id)
self.find_all_bttn.disconnect(self.find_all_handler_id)
self.replace_bttn.disconnect(self.replace_handler_id)
self.replace_all_bttn.disconnect(self.replace_all_handler_id)

View File

@@ -20,13 +20,27 @@ class Plugin(PluginCode):
...
def load(self):
tabs_controller = TabsController()
self.tabs_controller = TabsController()
code_container = self.request_ui_element("code-container")
self.register_controller("tabs", tabs_controller)
self.register_controller("tabs", self.tabs_controller)
code_container.add( tabs_controller.tabs_widget )
code_container.reorder_child(tabs_controller.tabs_widget, 0)
code_container.add( self.tabs_controller.tabs_widget )
code_container.reorder_child(self.tabs_controller.tabs_widget, 0)
event = Event_Factory.create_event("get_files")
self.emit_to("files", event)
for file in event.response:
self.tabs_controller.add_tab(file)
def unload(self):
self.unregister_controller("tabs")
self.tabs_controller.unload_tabs()
self.tabs_controller.tabs_widget.destroy()
self.tabs_controller.tabs_widget = None
self.tabs_controller = None
del self.tabs_controller
def run(self):
...

View File

@@ -32,7 +32,6 @@ class TabWidget(Gtk.Box):
self.set_orientation(0)
self.set_hexpand(False)
self.set_vexpand(False)
self.set_can_focus(False)
self.set_size_request(-1, 12)
def _setup_signals(self):
@@ -44,10 +43,6 @@ class TabWidget(Gtk.Box):
self.close_bttn = Gtk.Button()
icon = Gtk.Image(stock = Gtk.STOCK_CLOSE)
self.event_box.set_can_focus(False)
self.label.set_can_focus(False)
self.close_bttn.set_can_focus(False)
self.event_box.set_above_child(True)
ctx = self.label.get_style_context()
ctx.add_class("tab-label")

View File

@@ -16,7 +16,6 @@ from .tab_widget import TabWidget
class TabsController(ControllerBase):
def __init__(self):
super(TabsController, self).__init__()
@@ -35,7 +34,7 @@ class TabsController(ControllerBase):
elif isinstance(event, Code_Event_Types.FileExternallyDeletedEvent):
self.tabs_widget.externally_deleted( event.buffer )
elif isinstance(event, Code_Event_Types.AddedNewFileEvent):
self.add_tab(event)
self.add_tab(event.file)
elif isinstance(event, Code_Event_Types.PoppedFileEvent):
...
elif isinstance(event, Code_Event_Types.RemovedFileEvent):
@@ -53,11 +52,11 @@ class TabsController(ControllerBase):
break
def add_tab(self, event: Code_Event_Types.AddedNewFileEvent):
def add_tab(self, file):
tab = TabWidget()
tab.file = event.file
tab.file = file
tab.label.set_label(event.file.fname)
tab.label.set_label(file.fname)
self.tabs_widget.append_page(Gtk.Separator(), tab)
tab.show_all()
@@ -73,3 +72,13 @@ class TabsController(ControllerBase):
)
break
def unload_tabs(self):
for page_widget in self.tabs_widget.get_children():
tab = self.tabs_widget.get_tab_label(page_widget)
tab.clear_signals_and_data()
self.tabs_widget.remove_page(
self.tabs_widget.page_num(page_widget)
)

View File

@@ -33,6 +33,7 @@ class TabsWidget(Gtk.Notebook):
self.connect("page-added", self._page_added)
self.switch_page_id = \
self.connect_after("switch-page", self._switch_page)
self.connect("destroy", self._handle_destroy)
def _subscribe_to_events(self):
...
@@ -81,25 +82,38 @@ class TabsWidget(Gtk.Notebook):
)
def create_menu(self, page_widget) -> Gtk.Menu:
context_menu = Gtk.Menu()
context_menu = Gtk.Menu()
close_submenu = Gtk.Menu()
save_item = Gtk.MenuItem(label = "Save")
save_as_item = Gtk.MenuItem(label = "Save As")
close_item = Gtk.MenuItem(label = "Close Tab")
close_left_item = Gtk.MenuItem(label = "Close Tabs Left")
close_right_item = Gtk.MenuItem(label = "Close Tabs Right")
close_other_item = Gtk.MenuItem(label = "Close Other Tabs")
close_all_item = Gtk.MenuItem(label = "Close All Tabs")
close_actions_menu = Gtk.MenuItem(label = "Close Actions")
close_item = Gtk.MenuItem(label = "Close Tab")
close_left_item = Gtk.MenuItem(label = "Close Tabs Left")
close_right_item = Gtk.MenuItem(label = "Close Tabs Right")
close_other_item = Gtk.MenuItem(label = "Close Other Tabs")
close_all_item = Gtk.MenuItem(label = "Close All Tabs")
close_item.connect("activate", self.close_item, page_widget)
close_left_item.connect("activate", self.close_left_items, page_widget)
save_item.connect("activate", self.save_item, page_widget)
save_as_item.connect("activate", self.save_as_item, page_widget)
close_item.connect("activate", self.close_item, page_widget)
close_left_item.connect("activate", self.close_left_items, page_widget)
close_right_item.connect("activate", self.close_right_items, page_widget)
close_other_item.connect("activate", self.close_other_items, page_widget)
close_all_item.connect("activate", self.close_all_items, page_widget)
close_all_item.connect("activate", self.close_all_items, page_widget)
context_menu.append(close_item)
context_menu.append(close_left_item)
context_menu.append(close_right_item)
context_menu.append(close_other_item)
context_menu.append(close_all_item)
close_submenu.append(close_item)
close_submenu.append(close_left_item)
close_submenu.append(close_right_item)
close_submenu.append(close_other_item)
close_submenu.append(close_all_item)
close_actions_menu.set_submenu(close_submenu)
context_menu.append(save_item)
context_menu.append(save_as_item)
context_menu.append(close_actions_menu)
context_menu.show_all()
@@ -142,6 +156,14 @@ class TabsWidget(Gtk.Notebook):
break
def save_item(self, menu_item, page_widget):
tab = self.get_tab_label(page_widget)
tab.file.save()
def save_as_item(self, menu_item, page_widget):
tab = self.get_tab_label(page_widget)
tab.file.save_as()
def close_item(self, menu_item, page_widget):
tab = self.get_tab_label(page_widget)
tab.close_bttn.clicked()
@@ -176,3 +198,9 @@ class TabsWidget(Gtk.Notebook):
for widget in children[ : ]:
tab = self.get_tab_label(widget)
tab.close_bttn.clicked()
def _handle_destroy(self, widget):
self.disconnect_by_func(self._page_added)
self.disconnect_by_func(self._switch_page)
self.disconnect_by_func(self._handle_destroy)

View File

@@ -115,9 +115,9 @@ class ListBox(Gtk.ListBox):
self.select_row(next_row)
def add_row(self, event):
label = Gtk.Label(label = event.file.fname)
label.file = event.file
def add_row(self, file):
label = Gtk.Label(label = file.fname)
label.file = file
label.show()
self.add(label)

View File

@@ -25,7 +25,7 @@ class Plugin(PluginCode):
if isinstance(event, Code_Event_Types.FocusedViewEvent):
...
elif isinstance(event, Code_Event_Types.AddedNewFileEvent):
telescope.list_box.add_row(event)
telescope.list_box.add_row(event.file)
elif isinstance(event, Code_Event_Types.RemovedFileEvent):
telescope.list_box.remove_row(event)
elif isinstance(event, Code_Event_Types.FilePathSetEvent):
@@ -46,7 +46,7 @@ class Plugin(PluginCode):
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(
event = Event_Factory.create_event(
"create_source_view",
state = SourceViewStates.INDEPENDENT
)
@@ -55,12 +55,38 @@ class Plugin(PluginCode):
source_view = event.response
telescope.set_source_view(source_view)
event = Event_Factory.create_event(
event = Event_Factory.create_event(
"register_completer",
completer = source_view.get_completion()
)
self.emit_to("completion", event)
event = Event_Factory.create_event("get_files")
self.emit_to("files", event)
for file in event.response:
telescope.list_box.add_row(file)
def unload(self):
window = self.request_ui_element("main-window")
telescope.unmap_parent_resize_event(window)
event = Event_Factory.create_event("unregister_command",
command_name = "telescope",
command = Handler,
binding_mode = "released",
binding = "<Control>b"
)
self.emit_to("source_views", event)
event = Event_Factory.create_event(
"unregister_completer",
completer = telescope.source_view.get_completion()
)
self.emit_to("completion", event)
telescope.destroy()
def run(self):
...

View File

@@ -33,6 +33,7 @@ class Telescope(Gtk.Dialog):
def _setup_signals(self):
self.connect("focus-out-event", self._focus_out_event)
self.connect("show", self._show)
self.connect("destroy", self._handle_destroy)
def _subscribe_to_events(self):
...
@@ -93,7 +94,10 @@ class Telescope(Gtk.Dialog):
self.source_view.set_buffer(buffer)
def map_parent_resize_event(self, parent):
parent.connect("size-allocate", lambda w, r: self._map_resize(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()
@@ -103,3 +107,8 @@ class Telescope(Gtk.Dialog):
self.main_box.pack_end(scrolled_win, True, True, 0)
scrolled_win.show_all()
def _handle_destroy(self, widget):
self.disconnect_by_func(self._focus_out_event)
self.disconnect_by_func(self._show)
self.disconnect_by_func(self._handle_destroy)