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:
@@ -3,7 +3,7 @@
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from ..singleton_raised import SingletonRaised
|
||||
from ..singleton import Singleton
|
||||
|
||||
from ..dto.base_event import BaseEvent
|
||||
|
||||
@@ -17,7 +17,7 @@ class ControllerBaseException(Exception):
|
||||
|
||||
|
||||
|
||||
class ControllerBase(SingletonRaised, EmitDispatcher):
|
||||
class ControllerBase(Singleton, EmitDispatcher):
|
||||
def __init__(self):
|
||||
super(ControllerBase, self).__init__()
|
||||
|
||||
@@ -42,3 +42,6 @@ class ControllerBase(SingletonRaised, EmitDispatcher):
|
||||
|
||||
def register_controller(self, name: str, controller):
|
||||
self.controller_message_bus.register_controller(name, controller)
|
||||
|
||||
def unregister_controller(self, name: str):
|
||||
self.controller_message_bus.unregister_controller(name)
|
||||
|
||||
@@ -31,10 +31,11 @@ class ControllerManager(Singleton, dict):
|
||||
|
||||
|
||||
def _crete_controller_message_bus(self) -> ControllerMessageBus:
|
||||
controller_message_bus = ControllerMessageBus()
|
||||
controller_message_bus.message_to = self.message_to
|
||||
controller_message_bus.message = self.message
|
||||
controller_message_bus.register_controller = self.register_controller
|
||||
controller_message_bus = ControllerMessageBus()
|
||||
controller_message_bus.message_to = self.message_to
|
||||
controller_message_bus.message = self.message
|
||||
controller_message_bus.register_controller = self.register_controller
|
||||
controller_message_bus.unregister_controller = self.unregister_controller
|
||||
|
||||
return controller_message_bus
|
||||
|
||||
@@ -51,6 +52,17 @@ class ControllerManager(Singleton, dict):
|
||||
|
||||
self[name] = controller
|
||||
|
||||
def unregister_controller(self, name: str):
|
||||
if not name:
|
||||
raise ControllerManagerException("Must pass in a 'name'...")
|
||||
|
||||
if not name in self.keys():
|
||||
raise ControllerManagerException(
|
||||
f"Can't find controller registered with name of '{name}'..."
|
||||
)
|
||||
|
||||
self.pop(name, None)
|
||||
|
||||
def get_controllers_key_list(self) -> list[str]:
|
||||
return self.keys()
|
||||
|
||||
|
||||
@@ -28,3 +28,6 @@ class ControllerMessageBus:
|
||||
|
||||
def register_controller(self, name: str, controller):
|
||||
raise ControllerMessageBusException("Controller Message Bus 'register_controller' must be overriden by Controller Manager...")
|
||||
|
||||
def unregister_controller(self, name: str):
|
||||
raise ControllerMessageBusException("Controller Message Bus 'unregister_controller' must be overriden by Controller Manager...")
|
||||
|
||||
@@ -4,18 +4,21 @@
|
||||
|
||||
|
||||
from .code_event import CodeEvent
|
||||
from .toggle_plugins_ui_event import TogglePluginsUiEvent
|
||||
from .create_source_view_event import CreateSourceViewEvent
|
||||
from .register_completer_event import RegisterCompleterEvent
|
||||
from .unregister_completer_event import UnregisterCompleterEvent
|
||||
from .register_provider_event import RegisterProviderEvent
|
||||
from .unregister_provider_event import UnregisterProviderEvent
|
||||
from .register_command_event import RegisterCommandEvent
|
||||
from .unregister_command_event import UnregisterCommandEvent
|
||||
from .file_externally_modified_event import FileExternallyModifiedEvent
|
||||
from .file_externally_deleted_event import FileExternallyDeletedEvent
|
||||
from .set_info_labels_event import SetInfoLabelsEvent
|
||||
from .populate_source_view_popup_event import PopulateSourceViewPopupEvent
|
||||
from .filter_out_loaded_files_event import FilterOutLoadedFilesEvent
|
||||
from .get_active_view_event import GetActiveViewEvent
|
||||
from .get_source_views_event import GetSourceViewsEvent
|
||||
|
||||
from .get_new_command_system_event import GetNewCommandSystemEvent
|
||||
from .request_completion_event import RequestCompletionEvent
|
||||
@@ -34,6 +37,7 @@ from .removed_file_event import RemovedFileEvent
|
||||
from .saved_file_event import SavedFileEvent
|
||||
|
||||
from .get_file_event import GetFileEvent
|
||||
from .get_files_event import GetFilesEvent
|
||||
from .get_swap_file_event import GetSwapFileEvent
|
||||
from .add_new_file_event import AddNewFileEvent
|
||||
from .pop_file_event import PopFileEvent
|
||||
|
||||
13
src/libs/dto/code/events/get_files_event.py
Normal file
13
src/libs/dto/code/events/get_files_event.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Python imports
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .code_event import CodeEvent
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetFilesEvent(CodeEvent):
|
||||
...
|
||||
13
src/libs/dto/code/events/get_source_views_event.py
Normal file
13
src/libs/dto/code/events/get_source_views_event.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Python imports
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from .code_event import CodeEvent
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetSourceViewsEvent(CodeEvent):
|
||||
...
|
||||
17
src/libs/dto/code/events/toggle_plugins_ui_event.py
Normal file
17
src/libs/dto/code/events/toggle_plugins_ui_event.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Python imports
|
||||
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
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class TogglePluginsUiEvent(CodeEvent):
|
||||
...
|
||||
20
src/libs/dto/code/events/unregister_command_event.py
Normal file
20
src/libs/dto/code/events/unregister_command_event.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Python imports
|
||||
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
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnregisterCommandEvent(CodeEvent):
|
||||
command_name: str = ""
|
||||
command: callable = None
|
||||
binding_mode: str = ""
|
||||
binding: str or list = ""
|
||||
@@ -17,6 +17,7 @@ class Manifest:
|
||||
version: str = "0.0.1"
|
||||
support: str = "support@mail.com"
|
||||
pre_launch: bool = False
|
||||
autoload: bool = True
|
||||
requests: Requests = field(default_factory = lambda: Requests())
|
||||
|
||||
def __post_init__(self):
|
||||
|
||||
@@ -36,6 +36,19 @@ class EventFactory(Singleton):
|
||||
|
||||
logger.debug(f"Registered {i} event types:")
|
||||
|
||||
def unregister_events(self, events: dict):
|
||||
i = 0
|
||||
for name, obj in events:
|
||||
if not self._is_valid_event_class(obj): continue
|
||||
|
||||
event_type = self._class_name_to_event_type(name)
|
||||
|
||||
del self._event_classes[event_type]
|
||||
Code_Event_Types.remove_event_class(name)
|
||||
i += 1
|
||||
|
||||
logger.debug(f"Unregistered {i} event types:")
|
||||
|
||||
def create_event(self, event_type: str, **kwargs) -> BaseEvent:
|
||||
if event_type not in self._event_classes:
|
||||
raise ValueError(f"Unknown event type: {event_type}")
|
||||
@@ -80,6 +93,9 @@ class EventNamespace:
|
||||
def add_event_class(self, name: str, event_class: Type[BaseEvent]):
|
||||
setattr(self, name, event_class)
|
||||
|
||||
def remove_event_class(self, name: str):
|
||||
delattr(self, name)
|
||||
|
||||
|
||||
|
||||
Code_Event_Types = EventNamespace()
|
||||
|
||||
@@ -12,21 +12,21 @@ class SingletonError(Exception):
|
||||
|
||||
|
||||
|
||||
T = TypeVar('T', bound='Singleton')
|
||||
T = TypeVar('T', bound = 'Singleton')
|
||||
|
||||
|
||||
|
||||
class Singleton:
|
||||
__instance = None
|
||||
_instances = {}
|
||||
|
||||
def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T:
|
||||
if cls.__instance is not None:
|
||||
logger.debug(f"'{cls.__name__}' is a Singleton. Returning instance...")
|
||||
return cls.__instance
|
||||
if cls in cls._instances: return cls._instances[cls]
|
||||
|
||||
cls.__instance = super(Singleton, cls).__new__(cls)
|
||||
return cls.__instance
|
||||
instance = super().__new__(cls)
|
||||
cls._instances[cls] = instance
|
||||
return instance
|
||||
|
||||
def __init__(self) -> None:
|
||||
if self.__instance is not None:
|
||||
return
|
||||
|
||||
super(Singleton, self).__init__()
|
||||
@classmethod
|
||||
def destroy(cls):
|
||||
if cls in cls._instances:
|
||||
del cls._instances[cls]
|
||||
|
||||
Reference in New Issue
Block a user