From 356818b98cada45f775a334ee2f250755d739261 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Sat, 14 Feb 2026 16:12:58 -0600 Subject: [PATCH] Major completion provider overhaul; pluigin load and pattern improvements; css overhaul/cleanup; source view state modes added --- src/core/controllers/base_controller.py | 17 ++- src/core/widgets/code/code_base.py | 9 +- .../code/command_system/command_system.py | 4 +- .../commands/show_completion.py | 12 +- .../example_completion_provider.py | 84 ----------- .../lsp_completion_provider.py | 137 ------------------ .../provider_response_cache_base.py | 87 +++++++++++ .../python_completion_provider.py | 107 -------------- src/core/widgets/code/controllers/__init__.py | 2 +- .../code/controllers/completion_controller.py | 133 ++++++++--------- .../controllers/source_views_controller.py | 128 ---------------- .../code/controllers/views/__init__.py | 10 ++ .../code/controllers/views/signal_mapper.py | 63 ++++++++ .../views/source_views_controller.py | 74 ++++++++++ .../code/controllers/views/state_manager.py | 65 +++++++++ .../code/controllers/views/states/__init__.py | 8 + .../views/states/source_view_command_state.py | 36 +++++ .../views/states/source_view_insert_state.py | 71 +++++++++ .../states/source_view_multi_insert_state.py | 131 +++++++++++++++++ .../states/source_view_read_only_state.py | 36 +++++ src/core/widgets/code/key_mapper.py | 10 ++ .../code/mixins/source_mark_events_mixin.py | 107 ++++++++++++++ src/core/widgets/code/source_buffer.py | 31 +++- src/core/widgets/code/source_file.py | 39 ++++- src/core/widgets/code/source_view.py | 6 +- src/core/widgets/webkit/webkit_ui.py | 1 + src/libs/controllers/controller_base.py | 4 +- src/libs/controllers/controller_manager.py | 2 +- src/libs/dto/code/__init__.py | 3 + src/libs/dto/code/register_provider_event.py | 19 +++ src/libs/dto/code/saved_file_event.py | 13 ++ src/libs/dto/code/text_inserted_event.py | 11 +- src/libs/dto/states/__init__.py | 7 + src/libs/dto/states/cursor_action.py | 14 ++ src/libs/dto/states/move_direction.py | 15 ++ src/libs/dto/states/source_view_states.py | 14 ++ src/libs/settings/other/webkit_ui_settings.py | 3 +- src/plugins/controller.py | 4 +- src/plugins/manifest_manager.py | 5 +- src/plugins/plugin_types/__init__.py | 7 + src/plugins/plugin_types/plugin_base.py | 49 +++++++ src/plugins/plugin_types/plugin_code.py | 41 ++++++ .../plugin_ui.py} | 15 +- src/plugins/plugins_controller_mixin.py | 4 + 44 files changed, 1070 insertions(+), 568 deletions(-) delete mode 100644 src/core/widgets/code/completion_providers/example_completion_provider.py delete mode 100644 src/core/widgets/code/completion_providers/lsp_completion_provider.py create mode 100644 src/core/widgets/code/completion_providers/provider_response_cache_base.py delete mode 100644 src/core/widgets/code/completion_providers/python_completion_provider.py delete mode 100644 src/core/widgets/code/controllers/source_views_controller.py create mode 100644 src/core/widgets/code/controllers/views/__init__.py create mode 100644 src/core/widgets/code/controllers/views/signal_mapper.py create mode 100644 src/core/widgets/code/controllers/views/source_views_controller.py create mode 100644 src/core/widgets/code/controllers/views/state_manager.py create mode 100644 src/core/widgets/code/controllers/views/states/__init__.py create mode 100644 src/core/widgets/code/controllers/views/states/source_view_command_state.py create mode 100644 src/core/widgets/code/controllers/views/states/source_view_insert_state.py create mode 100644 src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py create mode 100644 src/core/widgets/code/controllers/views/states/source_view_read_only_state.py create mode 100644 src/core/widgets/code/mixins/source_mark_events_mixin.py create mode 100644 src/libs/dto/code/register_provider_event.py create mode 100644 src/libs/dto/code/saved_file_event.py create mode 100644 src/libs/dto/states/__init__.py create mode 100644 src/libs/dto/states/cursor_action.py create mode 100644 src/libs/dto/states/move_direction.py create mode 100644 src/libs/dto/states/source_view_states.py create mode 100644 src/plugins/plugin_types/__init__.py create mode 100644 src/plugins/plugin_types/plugin_base.py create mode 100644 src/plugins/plugin_types/plugin_code.py rename src/plugins/{plugin_base.py => plugin_types/plugin_ui.py} (68%) diff --git a/src/core/controllers/base_controller.py b/src/core/controllers/base_controller.py index 3aeb5cb..c95c9da 100644 --- a/src/core/controllers/base_controller.py +++ b/src/core/controllers/base_controller.py @@ -24,11 +24,14 @@ class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerMixin) def __init__(self): self._setup_controller_data() + + self._load_plugins(is_pre = True) self._setup_styling() self._setup_signals() self._subscribe_to_events() self._load_controllers() - self._load_plugins_and_files() + self._load_plugins(is_pre = False) + self._load_files() logger.info(f"Made it past {self.__class__} loading...") settings_manager.set_end_load_time() @@ -62,13 +65,19 @@ class BaseController(IPCSignalsMixin, KeyboardSignalsMixin, BaseControllerMixin) def _load_controllers(self): BridgeController() - def _load_plugins_and_files(self): + def _load_plugins(self, is_pre: bool): args, unknownargs = settings_manager.get_starting_args() + if args.no_plugins == "true": return - if args.no_plugins == "false": + if is_pre: self.plugins_controller.pre_launch_plugins() - self.plugins_controller.post_launch_plugins() + return + if not is_pre: + self.plugins_controller.post_launch_plugins() + return + + def _load_files(self): for file in settings_manager.get_starting_files(): event_system.emit("post-file-to-ipc", file) diff --git a/src/core/widgets/code/code_base.py b/src/core/widgets/code/code_base.py index 1b5dbb0..8013c08 100644 --- a/src/core/widgets/code/code_base.py +++ b/src/core/widgets/code/code_base.py @@ -11,7 +11,7 @@ from .controllers.files_controller import FilesController from .controllers.tabs_controller import TabsController from .controllers.commands_controller import CommandsController from .controllers.completion_controller import CompletionController -from .controllers.source_views_controller import SourceViewsController +from .controllers.views.source_views_controller import SourceViewsController from .mini_view_widget import MiniViewWidget @@ -50,7 +50,12 @@ class CodeBase: return self.miniview_widget def create_source_view(self): - return self.controller_manager["source_views"].create_source_view() + source_view = self.controller_manager["source_views"].create_source_view() + self.controller_manager["completion"].register_completer( + source_view.get_completion() + ) + + return source_view def first_map_load(self): self.controller_manager["source_views"].first_map_load() diff --git a/src/core/widgets/code/command_system/command_system.py b/src/core/widgets/code/command_system/command_system.py index 63986f2..48af7ed 100644 --- a/src/core/widgets/code/command_system/command_system.py +++ b/src/core/widgets/code/command_system/command_system.py @@ -39,11 +39,11 @@ class CommandSystem: def emit(self, event: Code_Event_Types.CodeEvent): - """ Monky patch 'emit' from command controller... """ + """ Monkey patch 'emit' from command controller... """ ... def emit_to(self, controller: str, event: Code_Event_Types.CodeEvent): - """ Monky patch 'emit' from command controller... """ + """ Monkey patch 'emit_to' from command controller... """ ... diff --git a/src/core/widgets/code/command_system/commands/show_completion.py b/src/core/widgets/code/command_system/commands/show_completion.py index dedb491..baec540 100644 --- a/src/core/widgets/code/command_system/commands/show_completion.py +++ b/src/core/widgets/code/command_system/commands/show_completion.py @@ -15,4 +15,14 @@ def execute( view: GtkSource.View = None ): logger.debug("Command: Show Completion") - view.command.request_completion(view) + completer = view.get_completion() + providers = completer.get_providers() + + if not providers: + view.command.request_completion(view) + return + + completer.start( + providers, + completer.create_context() + ) diff --git a/src/core/widgets/code/completion_providers/example_completion_provider.py b/src/core/widgets/code/completion_providers/example_completion_provider.py deleted file mode 100644 index 10f29a0..0000000 --- a/src/core/widgets/code/completion_providers/example_completion_provider.py +++ /dev/null @@ -1,84 +0,0 @@ -# Python imports -import re - -# 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 -from gi.repository import GObject - -# Application imports - - - -class ExampleCompletionProvider(GObject.GObject, GtkSource.CompletionProvider): - """ - This is a custom Completion Example Provider. - # NOTE: used information from here --> https://warroom.rsmus.com/do-that-auto-complete/ - """ - __gtype_name__ = 'ExampleCompletionProvider' - - def __init__(self): - GObject.Object.__init__(self) - - def do_get_name(self): - """ Returns: a new string containing the name of the provider. """ - return 'Example Completion Provider' - - def do_match(self, context): - """ Get whether the provider match the context of completion detailed in context. """ - # NOTE: True for debugging but context needs to normally get checked for actual usage needs. - # TODO: Fix me - return True - - def do_get_priority(self): - """ Determin position in result list along other providor results. """ - return 1 - - # def do_get_activation(self): - # """ The context for when a provider will show results """ - # return GtkSource.CompletionActivation.NONE - # return GtkSource.CompletionActivation.USER_REQUESTED - # return GtkSource.CompletionActivation.INTERACTIVE - - def do_populate(self, context): - """ - In this instance, it will do 2 things: - 1) always provide Hello World! (Not ideal but an option so its in the example) - 2) Utilizes the Gtk.TextIter from the TextBuffer to determine if there is a jinja - example of '{{ custom.' if so it will provide you with the options of foo and bar. - If selected it will insert foo }} or bar }}, completing your syntax... - - PLEASE NOTE the GtkTextIter Logic and regex are really rough and should be adjusted and tuned - """ - - proposals = [ - GtkSource.CompletionItem(label='Hello World!', text = 'Hello World!', icon = None, info = None) # NOTE: Always proposed... - ] - - # Gtk Versions differ on get_iter responses... - end_iter = context.get_iter() - if not isinstance(end_iter, Gtk.TextIter): - _, end_iter = context.get_iter() - - if end_iter: - buf = end_iter.get_buffer() - mov_iter = end_iter.copy() - if mov_iter.backward_search('{{', Gtk.TextSearchFlags.VISIBLE_ONLY): - mov_iter, _ = mov_iter.backward_search('{{', Gtk.TextSearchFlags.VISIBLE_ONLY) - left_text = buf.get_text(mov_iter, end_iter, True) - else: - left_text = '' - - if re.match(r'.*\{\{\s*custom\.$', left_text): - proposals.append( - GtkSource.CompletionItem(label='foo', text='foo }}') # optionally proposed based on left search via regex - ) - proposals.append( - GtkSource.CompletionItem(label='bar', text='bar }}') # optionally proposed based on left search via regex - ) - - context.add_proposals(self, proposals, True) \ No newline at end of file diff --git a/src/core/widgets/code/completion_providers/lsp_completion_provider.py b/src/core/widgets/code/completion_providers/lsp_completion_provider.py deleted file mode 100644 index 45b76ba..0000000 --- a/src/core/widgets/code/completion_providers/lsp_completion_provider.py +++ /dev/null @@ -1,137 +0,0 @@ -# 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 -from gi.repository import GObject - -# Application imports - - - -class LSPCompletionProvider(GObject.Object, GtkSource.CompletionProvider): - """ - This code is an LSP code completion plugin for Newton. - # NOTE: Some code pulled/referenced from here --> https://github.com/isamert/gedi - """ - __gtype_name__ = 'LSPProvider' - - def __init__(self): - GObject.Object.__init__(self) - - self._icon_theme = Gtk.IconTheme.get_default() - - self.lsp_data = None - - - def pre_populate(self, context): - ... - - def do_get_name(self): - return "LSP Code Completion" - - def get_iter_correctly(self, context): - return context.get_iter()[1] if isinstance(context.get_iter(), tuple) else context.get_iter() - - def do_match(self, context): - iter = self.get_iter_correctly(context) - iter.backward_char() - - buffer = iter.get_buffer() - if buffer.get_context_classes_at_iter(iter) != ['no-spell-check']: - return False - - ch = iter.get_char() - # NOTE: Look to re-add or apply supprting 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 - - return True - - def do_get_priority(self): - return 5 - - def do_populate(self, context, items = []): - # self.lsp_data - proposals = [] - - comp_item = GtkSource.CompletionItem.new() - comp_item.set_label("LSP Class") - comp_item.set_text("LSP Code") - # comp_item.set_icon(self.get_icon_for_type(completion.type)) - comp_item.set_info("A test LSP completion item...") - - context.add_proposals(self, [comp_item], True) - - - - - - - - - - - - - - - # def do_populate(self, context, items = []): - # if hasattr(self._source_view, "completion_items"): - # items = self._source_view.completion_items - - # proposals = [] - # for item in items: - # proposals.append( self.create_completion_item(item) ) - - # context.add_proposals(self, proposals, True) - - # def get_icon_for_type(self, _type): - # try: - # return self._theme.load_icon(icon_names[_type.lower()], 16, 0) - # except: - # ... - - # try: - # return self._theme.load_icon(Gtk.STOCK_ADD, 16, 0) - # except: - # ... - - # return None - - # def create_completion_item(self, item): - # comp_item = GtkSource.CompletionItem.new() - # keys = item.keys() - # comp_item.set_label(item["label"]) - - # if "insertText" in keys: - # comp_item.set_text(item["insertText"]) - - # if "additionalTextEdits" in keys: - # comp_item.additionalTextEdits = item["additionalTextEdits"] - - # return comp_item - - - # def create_completion_item(self, item): - # comp_item = GtkSource.CompletionItem.new() - # comp_item.set_label(item.label) - - # if item.textEdit: - # if isinstance(item.textEdit, dict): - # comp_item.set_text(item.textEdit["newText"]) - # else: - # comp_item.set_text(item.textEdit) - # else: - # comp_item.set_text(item.insertText) - - # comp_item.set_icon( self.get_icon_for_type(item.kind) ) - # comp_item.set_info(item.documentation) - - # return comp_item \ No newline at end of file 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 new file mode 100644 index 0000000..0ecce4d --- /dev/null +++ b/src/core/widgets/code/completion_providers/provider_response_cache_base.py @@ -0,0 +1,87 @@ +# Python imports +import re + +# 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 GtkSource + +# Application imports + + + +class ProviderResponseCacheException(Exception): + ... + + + +class ProviderResponseCacheBase: + def __init__(self): + super(ProviderResponseCacheBase, self).__init__() + + self._icon_theme = Gtk.IconTheme.get_default() + + + def process_file_load(self, buffer: GtkSource.Buffer): + raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_load' not implemented...") + + def process_file_close(self, buffer: GtkSource.Buffer): + raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_close' not implemented...") + + def process_file_save(self, buffer: GtkSource.Buffer): + raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_file_save' not implemented...") + + def process_file_change(self, buffer: GtkSource.Buffer): + raise ProviderResponseCacheException("ProviderResponseCacheBase 'process_change' not implemented...") + + def filter(self, word: str): + raise ProviderResponseCacheException("ProviderResponseCacheBase 'filter' not implemented...") + + def filter_with_context(self, context: GtkSource.CompletionContext): + raise ProviderResponseCacheException("ProviderResponseCacheBase 'filter_with_context' not implemented...") + + + def create_completion_item( + self, + label: str = "", + text: str = "", + info: str = "", + completion: any = None + ): + if not label or not text: return + + comp_item = GtkSource.CompletionItem.new() + comp_item.set_label(label) + comp_item.set_text(text) + + if info: + comp_item.set_info(info) + # comp_item.set_markup(f"

{info}

") + + if completion: + comp_item.set_icon( + self.get_icon_for_type(completion.type) + ) + + return comp_item + + def get_word(self, context): + start_iter = context.get_iter() + end_iter = None + + if not isinstance(start_iter, Gtk.TextIter): + _, start_iter = context.get_iter() + end_iter = start_iter.copy() + + if not start_iter.starts_word(): + start_iter.backward_word_start() + + end_iter.forward_word_end() + + buffer = start_iter.get_buffer() + + return buffer.get_text(start_iter, end_iter, False) diff --git a/src/core/widgets/code/completion_providers/python_completion_provider.py b/src/core/widgets/code/completion_providers/python_completion_provider.py deleted file mode 100644 index 319ce99..0000000 --- a/src/core/widgets/code/completion_providers/python_completion_provider.py +++ /dev/null @@ -1,107 +0,0 @@ -# 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 -from gi.repository import GObject - -import jedi -from jedi.api import Script - -# Application imports - - - -# FIXME: Find real icon names... -icon_names = { - 'import': '', - 'module': '', - 'class': '', - 'function': '', - 'statement': '', - 'param': '' -} - - -class Jedi: - def get_script(file, doc_text): - return Script(code = doc_text, path = file) - - -class PythonCompletionProvider(GObject.Object, GtkSource.CompletionProvider): - """ - This code is A python code completion plugin for Newton. - # NOTE: Some code pulled/referenced from here --> https://github.com/isamert/gedi - """ - __gtype_name__ = 'PythonProvider' - - def __init__(self, file): - GObject.Object.__init__(self) - self._theme = Gtk.IconTheme.get_default() - self._file = file - - def do_get_name(self): - return "Python Code Completion" - - def get_iter_correctly(self, context): - return context.get_iter()[1] if isinstance(context.get_iter(), tuple) else context.get_iter() - - def do_match(self, context): - iter = self.get_iter_correctly(context) - iter.backward_char() - - buffer = iter.get_buffer() - if buffer.get_context_classes_at_iter(iter) != ['no-spell-check']: - return False - - ch = iter.get_char() - # NOTE: Look to re-add or apply supprting 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 - - return True - - def do_get_priority(self): - return 1 - - def do_get_activation(self): - return GtkSource.CompletionActivation.INTERACTIVE - - def do_populate(self, context): - # TODO: Maybe convert async? - it = self.get_iter_correctly(context) - buffer = it.get_buffer() - proposals = [] - - doc_text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False) - iter_cursor = buffer.get_iter_at_mark(buffer.get_insert()) - linenum = iter_cursor.get_line() + 1 - charnum = iter_cursor.get_line_index() - - def create_generator(): - for completion in Jedi.get_script(self._file, doc_text).complete(line = linenum, column = None, fuzzy = False): - comp_item = GtkSource.CompletionItem.new() - comp_item.set_label(completion.name) - comp_item.set_text(completion.name) - comp_item.set_icon(self.get_icon_for_type(completion.type)) - comp_item.set_info(completion.docstring()) - yield comp_item - - for item in create_generator(): - proposals.append(item) - - context.add_proposals(self, proposals, True) - - def get_icon_for_type(self, _type): - try: - return self._theme.load_icon(icon_names[_type.lower()], 16, 0) - except (KeyError, AttributeError, GObject.GError) as e: - return self._theme.load_icon(Gtk.STOCK_ADD, 16, 0) - except (GObject.GError, AttributeError) as e: - return None \ No newline at end of file diff --git a/src/core/widgets/code/controllers/__init__.py b/src/core/widgets/code/controllers/__init__.py index e614b70..8f3b747 100644 --- a/src/core/widgets/code/controllers/__init__.py +++ b/src/core/widgets/code/controllers/__init__.py @@ -1,3 +1,3 @@ """ Code Controllers Package -""" \ No newline at end of file +""" diff --git a/src/core/widgets/code/controllers/completion_controller.py b/src/core/widgets/code/controllers/completion_controller.py index 6330841..91dec63 100644 --- a/src/core/widgets/code/controllers/completion_controller.py +++ b/src/core/widgets/code/controllers/completion_controller.py @@ -11,93 +11,84 @@ from gi.repository import GtkSource from libs.controllers.controller_base import ControllerBase from libs.event_factory import Event_Factory, Code_Event_Types -from ..completion_providers.example_completion_provider import ExampleCompletionProvider -from ..completion_providers.lsp_completion_provider import LSPCompletionProvider - class CompletionController(ControllerBase): def __init__(self): super(CompletionController, self).__init__() - self._completor: GtkSource.Completion = None - self._timeout_id: int = None - self._lsp_provider: LSPCompletionProvider = LSPCompletionProvider() + self.words_provider = GtkSource.CompletionWords.new("words", None) + self.words_provider.props.activation = GtkSource.CompletionActivation.INTERACTIVE + self._completers: list[GtkSource.Completion] = [] + self._providers: dict[str, GtkSource.CompletionProvider] = {} def _controller_message(self, event: Code_Event_Types.CodeEvent): - if isinstance(event, Code_Event_Types.FocusedViewEvent): - self._completor = event.view.get_completion() - - if not self._timeout_id: return - - GLib.source_remove(self._timeout_id) - self._timeout_id = None - elif isinstance(event, Code_Event_Types.RequestCompletionEvent): - self.request_completion() - # elif isinstance(event, Code_Event_Types.TextInsertedEvent): - # self.request_completion() - - def _process_request_completion(self): - self._start_completion() - - self._timeout_id = None - return False - - def _do_completion(self): - if self._completor.get_providers(): - self._match_completion() - else: - self._start_completion() - - def _match_completion(self): - """ - Note: Use IF providers were added to completion... - """ - self._completion.match( - self._completion.create_context() - ) + if isinstance(event, Code_Event_Types.RegisterProviderEvent): + self.register_provider( + event.provider_name, + event.provider, + event.language_ids + ) + elif isinstance(event, Code_Event_Types.AddedNewFileEvent): + self.provider_process_file_load(event) + elif isinstance(event, Code_Event_Types.RemovedFileEvent): + 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.RequestCompletionEvent): + # self.request_unbound_completion( event.view.get_completion() ) - def _start_completion(self): - """ - Note: Use IF NO providers have been added to completion... - """ - self._completor.start( - [ - ExampleCompletionProvider(), - self._lsp_provider - ], - self._completor.create_context() - ) + def register_completer(self, completer: GtkSource.Completion): + self._completers.append(completer) - - def set_completer(self, completer): - self._completor = completer - - def request_completion(self): - if self._timeout_id: - GLib.source_remove(self._timeout_id) - - self._timeout_id = GLib.timeout_add( - 800, - self._process_request_completion - ) + completer.add_provider(self.words_provider) + for provider in self._providers.values(): + completer.add_provider(provider) def register_provider( self, provider_name: str, - provider: GtkSource.CompletionProvider, - priority: int = 0, - language_ids: list = None + provider: GtkSource.CompletionProvider, + language_ids: list = [] ): - """Register completion providers with priority and language filtering""" - ... + self._providers[provider_name] = provider + + for completer in self._completers: + completer.add_provider(provider) def unregister_provider(self, provider_name: str): - """Remove completion providers""" - ... - - def get_active_providers(self, language_id: str = None) -> list: - """Get providers filtered by language""" - ... + provider = self._providers[provider_name] + del self._providers[provider_name] + + for completer in self._completers: + completer.remove_provider(provider) + + def provider_process_file_load(self, event: Code_Event_Types.AddedNewFileEvent): + self.words_provider.register(event.file.buffer) + + for provider in self._providers.values(): + provider.response_cache.process_file_load(event) + + def provider_process_file_close(self, event: Code_Event_Types.RemovedFileEvent): + self.words_provider.unregister(event.file.buffer) + + for provider in self._providers.values(): + provider.response_cache.process_file_close(event) + + def provider_process_file_save(self, event: Code_Event_Types.SavedFileEvent): + for provider in self._providers.values(): + provider.response_cache.process_file_save(event) + + def provider_process_file_change(self, event: Code_Event_Types.TextChangedEvent): + for provider in self._providers.values(): + provider.response_cache.process_file_change(event) + + def request_unbound_completion(self, completer: GtkSource.Completion): + completer.start( + [ *self._providers.values() ], + completer.create_context() + ) diff --git a/src/core/widgets/code/controllers/source_views_controller.py b/src/core/widgets/code/controllers/source_views_controller.py deleted file mode 100644 index c2ff44d..0000000 --- a/src/core/widgets/code/controllers/source_views_controller.py +++ /dev/null @@ -1,128 +0,0 @@ -# Python imports - -# Lib imports - -# Application imports -from libs.controllers.controller_base import ControllerBase -from libs.event_factory import Event_Factory, Code_Event_Types - -from ..command_system import CommandSystem -from ..key_mapper import KeyMapper - -from ..source_view import SourceView - - - -class SourceViewsController(ControllerBase, list): - def __init__(self): - super(SourceViewsController, self).__init__() - - self.key_mapper: KeyMapper = KeyMapper() - self.active_view: SourceView = None - - - def get_command_system(self): - event = Event_Factory.create_event("get_command_system") - self.message_to("commands", event) - command = event.response - - del event - return command - - def create_source_view(self): - source_view: SourceView = SourceView() - source_view.command = self.get_command_system() - source_view.command.set_data(source_view) - - self._map_signals(source_view) - - self.append(source_view) - return source_view - - def _controller_message(self, event: Code_Event_Types.CodeEvent): - if isinstance(event, Code_Event_Types.RemovedFileEvent): - self._remove_file(event) - elif isinstance(event, Code_Event_Types.TextChangedEvent): - self.active_view.command.exec("update_info_bar") - - def _map_signals(self, source_view: SourceView): - source_view.connect("focus-in-event", self._focus_in_event) - source_view.connect("move-cursor", self._move_cursor) - source_view.connect("key-press-event", self._key_press_event) - source_view.connect("key-release-event", self._key_release_event) - source_view.connect("button-press-event", self._button_press_event) - source_view.connect("button-release-event", self._button_release_event) - - def _focus_in_event(self, view, eve): - self.active_view = view - - view.command.exec("set_miniview") - view.command.exec("set_focus_border") - view.command.exec("update_info_bar") - - event = Event_Factory.create_event("focused_view", view = view) - self.emit(event) - - def _move_cursor(self, view, step, count, extend_selection): - buffer = view.get_buffer() - iter = buffer.get_iter_at_mark( buffer.get_insert() ) - line = iter.get_line() - char = iter.get_line_offset() - - event = Event_Factory.create_event( - "cursor_moved", - view = view, - buffer = buffer, - line = line, - char = char - ) - - self.emit(event) - - view.command.exec("update_info_bar") - - def _button_press_event(self, view, eve): - self.active_view.command.exec("update_info_bar") - - def _button_release_event(self, view, eve): - self.active_view.command.exec("update_info_bar") - - def _key_press_event(self, view, eve): - command = self.key_mapper._key_press_event(eve) - is_future = self.key_mapper._key_release_event(eve) - - if is_future: return True - if not command: return False - - view.command.exec(command) - - return True - - def _key_release_event(self, view, eve): - command = self.key_mapper._key_release_event(eve) - is_past = self.key_mapper._key_press_event(eve) - - if is_past: return True - if not command: return False - - view.command.exec(command) - - return True - - def _remove_file(self, event: Code_Event_Types.RemovedFileEvent): - for view in self: - if not event.file.buffer == view.get_buffer(): continue - if not event.next_file: - view.command.exec("new_file") - continue - - view.set_buffer(event.next_file.buffer) - - def first_map_load(self): - for view in self: - view.command.exec("new_file") - - view = self[0] - view.grab_focus() - view.command.exec("load_start_files") - diff --git a/src/core/widgets/code/controllers/views/__init__.py b/src/core/widgets/code/controllers/views/__init__.py new file mode 100644 index 0000000..1461e87 --- /dev/null +++ b/src/core/widgets/code/controllers/views/__init__.py @@ -0,0 +1,10 @@ +""" + Code Controllers Package +""" + +from .state_manager import SourceViewStateManager +from .signal_mapper import SourceViewSignalMapper +from .source_views_controller import SourceViewsController + +# State imports +from .states import * \ No newline at end of file diff --git a/src/core/widgets/code/controllers/views/signal_mapper.py b/src/core/widgets/code/controllers/views/signal_mapper.py new file mode 100644 index 0000000..8577f57 --- /dev/null +++ b/src/core/widgets/code/controllers/views/signal_mapper.py @@ -0,0 +1,63 @@ +# Python imports + +# Lib imports + +# Application imports +from ...source_view import SourceView + + + +class SourceViewSignalMapper: + def __init__(self): + self.active_view: SourceView = None + + + def bind_emit(self, emit: callable): + self.emit = emit + + def set_state_manager(self, state_manager): + self.state_manager = state_manager + + def connect_signals(self, source_view: SourceView): + signal_mappings = self._get_signal_mappings() + for signal, handler in signal_mappings.items(): + source_view.connect(signal, handler) + + def disconnect_signals(self, source_view: SourceView): + signal_mappings = self._get_signal_mappings() + for signal, handler in signal_mappings.items(): + source_view.disconnect_by_func(handler) + + def insert_text(self, file, string: str): + return self.state_manager.handle_insert_text(self.active_view, file, string) + + def _get_signal_mappings(self): + return { + "focus-in-event": self._focus_in_event, + "move-cursor": self._move_cursor, + "key-press-event": self._key_press_event, + "key-release-event": self._key_release_event, + "button-press-event": self._button_press_event, + "button-release-event": self._button_release_event + } + + def _focus_in_event(self, source_view: SourceView, eve): + self.active_view = source_view + return self.state_manager.handle_focus_in_event(source_view, eve, self.emit) + + def _move_cursor(self, source_view: SourceView, step, count, extend_selection): + return self.state_manager.handle_move_cursor( + source_view, step, count, extend_selection, self.emit + ) + + def _button_press_event(self, source_view: SourceView, eve): + return self.state_manager.handle_button_press_event(source_view, eve) + + def _button_release_event(self, source_view: SourceView, eve): + return self.state_manager.handle_button_release_event(source_view, eve) + + def _key_press_event(self, source_view: SourceView, eve): + return self.state_manager.handle_key_press_event(source_view, eve) + + def _key_release_event(self, source_view: SourceView, eve): + return self.state_manager.handle_key_release_event(source_view, eve) diff --git a/src/core/widgets/code/controllers/views/source_views_controller.py b/src/core/widgets/code/controllers/views/source_views_controller.py new file mode 100644 index 0000000..08c914e --- /dev/null +++ b/src/core/widgets/code/controllers/views/source_views_controller.py @@ -0,0 +1,74 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.controllers.controller_base import ControllerBase +from libs.event_factory import Event_Factory, Code_Event_Types + +from ...source_view import SourceView + +from .state_manager import SourceViewStateManager +from .signal_mapper import SourceViewSignalMapper + + + +class SourceViewsController(ControllerBase, list): + def __init__(self): + super(SourceViewsController, self).__init__() + + self.state_manager: SourceViewStateManager = SourceViewStateManager() + self.signal_mapper: SourceViewSignalMapper = SourceViewSignalMapper() + + self.signal_mapper.bind_emit(self.emit) + self.signal_mapper.set_state_manager(self.state_manager) + + + def _controller_message(self, event: Code_Event_Types.CodeEvent): + if isinstance(event, Code_Event_Types.RemovedFileEvent): + self._remove_file(event) + + if not self.signal_mapper.active_view: return + + if isinstance(event, Code_Event_Types.TextChangedEvent): + if not self.signal_mapper.active_view: return + self.signal_mapper.active_view.command.exec("update_info_bar") + elif isinstance(event, Code_Event_Types.TextChangedEvent): + self.signal_mapper.active_view.command.exec("update_info_bar") + elif isinstance(event, Code_Event_Types.TextInsertedEvent): + self.signal_mapper.insert_text(event.file, event.text) + + def _get_command_system(self): + event = Event_Factory.create_event("get_command_system") + self.message_to("commands", event) + command = event.response + + del event + return command + + def _remove_file(self, event: Code_Event_Types.RemovedFileEvent): + for source_view in self: + if not event.file.buffer == source_view.get_buffer(): continue + if not event.next_file: + source_view.command.exec("new_file") + continue + + source_view.set_buffer(event.next_file.buffer) + + def create_source_view(self): + source_view: SourceView = SourceView() + source_view.command = self._get_command_system() + source_view.command.set_data(source_view) + + self.signal_mapper.connect_signals(source_view) + + self.append(source_view) + return source_view + + def first_map_load(self): + for source_view in self: + source_view.command.exec("new_file") + + source_view = self[0] + source_view.grab_focus() + source_view.command.exec("load_start_files") diff --git a/src/core/widgets/code/controllers/views/state_manager.py b/src/core/widgets/code/controllers/views/state_manager.py new file mode 100644 index 0000000..8bae1a1 --- /dev/null +++ b/src/core/widgets/code/controllers/views/state_manager.py @@ -0,0 +1,65 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.dto.states import SourceViewStates + +from ...key_mapper import KeyMapper + +from .states import * + + +class SourceViewStateManager: + def __init__(self): + self.key_mapper: KeyMapper = KeyMapper() + + self.states: dict = { + SourceViewStates.INSERT: SourceViewsInsertState(), + SourceViewStates.MULTIINSERT: SourceViewsMultiInsertState(), + SourceViewStates.COMMAND: SourceViewsCommandState(), + SourceViewStates.READONLY: SourceViewsReadOnlyState() + } + + + def handle_focus_in_event(self, source_view, eve, emit): + return self.states[source_view.state].focus_in_event(source_view, eve, emit) + + def handle_insert_text(self, source_view, file, text): + return self.states[source_view.state].insert_text(file, text) + + def handle_move_cursor(self, source_view, step, count, extend_selection, emit): + return self.states[source_view.state].move_cursor( + source_view, step, count, extend_selection, emit + ) + + def handle_button_press_event(self, source_view, eve): + return self.states[source_view.state].button_press_event(source_view, eve) + + def handle_button_release_event(self, source_view, eve): + # Handle state transitions (multi-insert toggling) + self._handle_multi_insert_toggle(source_view, eve) + + return self.states[source_view.state].button_release_event(source_view, eve) + + def handle_key_press_event(self, source_view, eve): + return self.states[source_view.state].key_press_event( + source_view, eve, self.key_mapper + ) + + def handle_key_release_event(self, source_view, eve): + return self.states[source_view.state].key_release_event( + source_view, eve, self.key_mapper + ) + + def _handle_multi_insert_toggle(self, source_view, eve): + is_control = self.key_mapper.is_control(eve) + if is_control and not source_view.state == SourceViewStates.MULTIINSERT: + logger.debug("Entered Multi-Insert Mode...") + source_view.state = SourceViewStates.MULTIINSERT + + if not is_control and source_view.state == SourceViewStates.MULTIINSERT: + logger.debug("Entered Regular Insert Mode...") + self.states[source_view.state].clear_markers(source_view) + + source_view.state = SourceViewStates.INSERT diff --git a/src/core/widgets/code/controllers/views/states/__init__.py b/src/core/widgets/code/controllers/views/states/__init__.py new file mode 100644 index 0000000..9b79f5c --- /dev/null +++ b/src/core/widgets/code/controllers/views/states/__init__.py @@ -0,0 +1,8 @@ +""" + Code Controllers Views States Package +""" + +from .source_view_insert_state import SourceViewsInsertState +from .source_view_multi_insert_state import SourceViewsMultiInsertState +from .source_view_command_state import SourceViewsCommandState +from .source_view_read_only_state import SourceViewsReadOnlyState diff --git a/src/core/widgets/code/controllers/views/states/source_view_command_state.py b/src/core/widgets/code/controllers/views/states/source_view_command_state.py new file mode 100644 index 0000000..d57d808 --- /dev/null +++ b/src/core/widgets/code/controllers/views/states/source_view_command_state.py @@ -0,0 +1,36 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from libs.dto.states import SourceViewStates + + + +class SourceViewsCommandState: + def __init__(self): + super(SourceViewsCommandState, self).__init__() + + + def focus_in_event(self, source_view, eve, emit): + return True + + def move_cursor(self, source_view, step, count, extend_selection, emit): + return True + + def insert_text(self, file, text): + return True + + def button_press_event(self, source_view, eve): + return True + + def button_release_event(self, source_view, eve): + return True + + def key_press_event(self, source_view, eve, key_mapper): + return True + + def key_release_event(self, source_view, eve, key_mapper): + return True diff --git a/src/core/widgets/code/controllers/views/states/source_view_insert_state.py b/src/core/widgets/code/controllers/views/states/source_view_insert_state.py new file mode 100644 index 0000000..7a2bd66 --- /dev/null +++ b/src/core/widgets/code/controllers/views/states/source_view_insert_state.py @@ -0,0 +1,71 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from libs.dto.states import SourceViewStates + + + +class SourceViewsInsertState: + def __init__(self): + super(SourceViewsInsertState, self).__init__() + + + def focus_in_event(self, source_view, eve, emit): + source_view.command.exec("set_miniview") + source_view.command.exec("set_focus_border") + source_view.command.exec("update_info_bar") + + event = Event_Factory.create_event("focused_view", view = source_view) + emit(event) + + def insert_text(self, file, text): + return True + + def move_cursor(self, source_view, step, count, extend_selection, emit): + buffer = source_view.get_buffer() + itr = buffer.get_iter_at_mark( buffer.get_insert() ) + line = itr.get_line() + char = itr.get_line_offset() + event = Event_Factory.create_event( + "cursor_moved", + view = source_view, + buffer = buffer, + line = line, + char = char + ) + + emit(event) + + source_view.command.exec("update_info_bar") + + def button_press_event(self, source_view, eve): + source_view.command.exec("update_info_bar") + + def button_release_event(self, source_view, eve): + source_view.command.exec("update_info_bar") + + def key_press_event(self, source_view, eve, key_mapper): + command = key_mapper._key_press_event(eve) + is_future = key_mapper._key_release_event(eve) + + if is_future: return True + if not command: return False + + source_view.command.exec(command) + + return True + + def key_release_event(self, source_view, eve, key_mapper): + command = key_mapper._key_release_event(eve) + is_past = key_mapper._key_press_event(eve) + + if is_past: return True + if not command: return False + + source_view.command.exec(command) + + return True diff --git a/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py b/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py new file mode 100644 index 0000000..8de34be --- /dev/null +++ b/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py @@ -0,0 +1,131 @@ +# Python imports + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types +from libs.dto.states import SourceViewStates, MoveDirection, CursorAction + +from ....mixins.source_mark_events_mixin import MarkEventsMixin + + + +class SourceViewsMultiInsertState(MarkEventsMixin): + def __init__(self): + super(SourceViewsMultiInsertState, self).__init__() + + self.cursor_action: CursorAction = 0 + self.move_direction: MoveDirection = 0 + self.insert_markers: list = [] + + + def focus_in_event(self, source_view, eve, emit): + source_view.command.exec("set_miniview") + source_view.command.exec("set_focus_border") + source_view.command.exec("update_info_bar") + + event = Event_Factory.create_event("focused_view", view = source_view) + emit(event) + + def insert_text(self, file, text): + if not self.insert_markers: return False + + buffer = file.buffer + + # freeze buffer and insert to each mark (if any) + buffer.block_insert_after_signal() + buffer.begin_user_action() + + with buffer.freeze_notify(): + for mark in self.insert_markers: + itr = buffer.get_iter_at_mark(mark) + buffer.insert(itr, text, -1) + + buffer.end_user_action() + buffer.unblock_insert_after_signal() + + return True + + def move_cursor(self, source_view, step, count, extend_selection, emit): + buffer = source_view.get_buffer() + + self._process_move_direction(buffer) + self._signal_cursor_moved(source_view, emit) + source_view.command.exec("update_info_bar") + + def button_press_event(self, source_view, eve): + source_view.command.exec("update_info_bar") + return True + + def button_release_event(self, source_view, eve): + buffer = source_view.get_buffer() + insert_iter = buffer.get_iter_at_mark( buffer.get_insert() ) + data = source_view.window_to_buffer_coords( + Gtk.TextWindowType.TEXT, + eve.x, + eve.y + ) + is_over_text, \ + target_iter, \ + is_trailing = source_view.get_iter_at_position(data.buffer_x, data.buffer_y) + + if not is_over_text: + # NOTE: Trying to put at very end of line if not over text (aka, clicking right of text) + target_iter.forward_visible_line() + target_iter.backward_char() + + self._insert_mark(insert_iter, target_iter, buffer) + + def key_press_event(self, source_view, eve, key_mapper): + char = key_mapper.get_raw_keyname(eve) + + for action in CursorAction: + if not action.name == char.upper(): continue + self.cursor_action = action.value + self._process_cursor_action(source_view.get_buffer()) + + return False + + for direction in MoveDirection: + if not direction.name == char.upper(): continue + self.move_direction = direction.value + return False + + is_future = key_mapper._key_release_event(eve) + if is_future: return False + + command = key_mapper._key_press_event(eve) + if not command: return False + + source_view.command.exec(command) + + return True + + def key_release_event(self, source_view, eve, key_mapper): + command = key_mapper._key_release_event(eve) + is_past = key_mapper._key_press_event(eve) + + if is_past: return False + if not command: return False + + source_view.command.exec(command) + + return True + + def _signal_cursor_moved(self, source_view, emit): + buffer = source_view.get_buffer() + itr = buffer.get_iter_at_mark( buffer.get_insert() ) + line = itr.get_line() + char = itr.get_line_offset() + event = Event_Factory.create_event( + "cursor_moved", + view = source_view, + buffer = buffer, + line = line, + char = char + ) + + emit(event) diff --git a/src/core/widgets/code/controllers/views/states/source_view_read_only_state.py b/src/core/widgets/code/controllers/views/states/source_view_read_only_state.py new file mode 100644 index 0000000..07317d2 --- /dev/null +++ b/src/core/widgets/code/controllers/views/states/source_view_read_only_state.py @@ -0,0 +1,36 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.event_factory import Event_Factory, Code_Event_Types + +from libs.dto.states import SourceViewStates + + + +class SourceViewsReadOnlyState: + def __init__(self): + super(SourceViewsReadOnlyState, self).__init__() + + + def focus_in_event(self, source_view, eve, emit): + return True + + def move_cursor(self, source_view, step, count, extend_selection, emit): + return True + + def insert_text(self, file, text): + return True + + def button_press_event(self, source_view, eve): + return True + + def button_release_event(self, source_view, eve): + return True + + def key_press_event(self, source_view, eve, key_mapper): + return True + + def key_release_event(self, source_view, eve, key_mapper): + return True diff --git a/src/core/widgets/code/key_mapper.py b/src/core/widgets/code/key_mapper.py index 9ddfb96..9193401 100644 --- a/src/core/widgets/code/key_mapper.py +++ b/src/core/widgets/code/key_mapper.py @@ -125,3 +125,13 @@ class KeyMapper: if is_alt: self.state = self.state | AltKeyState + def is_control(self, eve): + modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK) + return True if modifiers & Gdk.ModifierType.CONTROL_MASK else False + + def is_shift(self, eve): + modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK) + return True if modifiers & Gdk.ModifierType.SHIFT_MASK else False + + def get_raw_keyname(self, eve): + return Gdk.keyval_name(eve.keyval) diff --git a/src/core/widgets/code/mixins/source_mark_events_mixin.py b/src/core/widgets/code/mixins/source_mark_events_mixin.py new file mode 100644 index 0000000..460d70c --- /dev/null +++ b/src/core/widgets/code/mixins/source_mark_events_mixin.py @@ -0,0 +1,107 @@ +# Python imports +import random + +# Lib imports +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk + +# Application imports +from libs.dto.states import SourceViewStates, MoveDirection, CursorAction + + + +class MarkEventsMixin: + def clear_markers(self, source_view): + buffer = source_view.get_buffer() + + for mark in self.insert_markers: + mark.set_visible(False) + buffer.delete_mark(mark) + + self.insert_markers.clear() + + def _insert_mark(self, insert_iter, target_iter, buffer): + mark_found = self._check_for_insert_marks(target_iter, buffer) + + if mark_found: return + + random_bits = random.getrandbits(128) + hash = "%032x" % random_bits + + mark = Gtk.TextMark.new( + name = f"multi_insert_{hash}", + left_gravity = False + ) + + buffer.add_mark(mark, target_iter) + mark.set_visible(True) + self.insert_markers.append(mark) + + + def _check_for_insert_marks(self, target_iter, buffer): + marks = target_iter.get_marks() + + for mark in marks: + if mark in self.insert_markers[:]: + mark.set_visible(False) + self.insert_markers.remove(mark) + buffer.delete_mark(mark) + return True + + insert_itr = buffer.get_iter_at_mark( buffer.get_insert() ) + if target_iter.equal(insert_itr): return True + + return False + + def _process_cursor_action(self, buffer): + if not self.insert_markers: return + if self.cursor_action == CursorAction.NONE.value: return + + action = self.cursor_action + for mark in self.insert_markers: + itr = buffer.get_iter_at_mark(mark) + + if action == CursorAction.BACKSPACE.value: + buffer.backspace(itr, interactive = True, default_editable = True) + elif action == CursorAction.DELETE.value: + itr.forward_char() + buffer.backspace(itr, interactive = True, default_editable = True) + elif action == CursorAction.ENTER.value: + ... + + self.cursor_action = CursorAction.NONE.value + + def _process_move_direction(self, buffer): + if not self.insert_markers: return + if self.move_direction == MoveDirection.NONE.value: return + + direction = self.move_direction + for mark in self.insert_markers: + itr = buffer.get_iter_at_mark(mark) + + if direction == MoveDirection.UP.value: + new_line = itr.get_line() - 1 + new_itr = buffer.get_iter_at_line_offset( + new_line, + itr.get_line_index() + ) + + elif direction == MoveDirection.DOWN.value: + new_line = itr.get_line() + 1 + new_itr = buffer.get_iter_at_line_offset( + new_line, + itr.get_line_index() + ) + elif direction == MoveDirection.LEFT.value: + if not itr.backward_char(): break + new_itr = itr + elif direction == MoveDirection.RIGHT.value: + if not itr.forward_char(): break + new_itr = itr + else: + continue + + buffer.move_mark_by_name(mark.get_name(), new_itr) + + self.move_direction = MoveDirection.NONE diff --git a/src/core/widgets/code/source_buffer.py b/src/core/widgets/code/source_buffer.py index 8c70296..083d15c 100644 --- a/src/core/widgets/code/source_buffer.py +++ b/src/core/widgets/code/source_buffer.py @@ -21,20 +21,41 @@ class SourceBuffer(GtkSource.Buffer): _changed, _mark_set, _insert_text, + _after_insert_text, _modified_changed, ): self._handler_ids = [ - self.connect("changed", _changed), - self.connect("mark-set", _mark_set), - self.connect("insert-text", _insert_text), - self.connect("modified-changed", _modified_changed) + self.connect("changed", _changed), + 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) ] + def block_changed_signal(self): + self.handler_block(self._handler_ids[0]) + + def block_insert_after_signal(self): + self.handler_block(self._handler_ids[3]) + + def block_modified_changed_signal(self): + self.handler_block(self._handler_ids[4]) + + def unblock_changed_signal(self): + self.handler_unblock(self._handler_ids[0]) + + def unblock_insert_after_signal(self): + self.handler_unblock(self._handler_ids[3]) + + def unblock_modified_changed_signal(self): + self.handler_unblock(self._handler_ids[4]) + def clear_signals(self): for handle_id in self._handler_ids: self.disconnect(handle_id) def __del__(self): for handle_id in self._handler_ids: - self.disconnect(handle_id) \ No newline at end of file + self.disconnect(handle_id) + diff --git a/src/core/widgets/code/source_file.py b/src/core/widgets/code/source_file.py index 02817c1..43288af 100644 --- a/src/core/widgets/code/source_file.py +++ b/src/core/widgets/code/source_file.py @@ -8,6 +8,7 @@ gi.require_version('Gtk', '3.0') gi.require_version('GtkSource', '4') from gi.repository import Gtk +from gi.repository import GLib from gi.repository import GtkSource from gi.repository import Gio @@ -37,6 +38,7 @@ class SourceFile(GtkSource.File): self._changed, self._mark_set, self._insert_text, + self._after_insert_text, self._modified_changed ) @@ -45,17 +47,38 @@ class SourceFile(GtkSource.File): event.file = self self.emit(event) - def _insert_text(self, buffer: SourceBuffer, location: Gtk.TextIter, + def _insert_text( + self, + buffer: SourceBuffer, + location: Gtk.TextIter, + text: str, length: int + ): + ... + + def _after_insert_text( + self, + buffer: SourceBuffer, + location: Gtk.TextIter, text: str, length: int ): event = Event_Factory.create_event( "text_inserted", - file = self, - buffer = buffer + file = self, + buffer = self.buffer, + location = location, + text = text, + length = length ) - self.emit(event) - def _mark_set(self, buffer: SourceBuffer, location: Gtk.TextIter, + # Note: 'idle_add' needed b/c markers don't get thir positions + # updated relative to the initial insert. + # If not used, seg faults galor during multi insert. + GLib.idle_add(self.emit, event) + + def _mark_set( + self, + buffer: SourceBuffer, + location: Gtk.TextIter, mark: Gtk.TextMark ): # event = Event_Factory.create_event( @@ -108,6 +131,12 @@ class SourceFile(GtkSource.File): self.emit(event) def save(self): + event = Event_Factory.create_event( + "saved_file", + file = self, buffer = self.buffer + ) + + self.emit(event) self._write_file( self.get_location() ) def save_as(self): diff --git a/src/core/widgets/code/source_view.py b/src/core/widgets/code/source_view.py index ce48f6b..bbb752d 100644 --- a/src/core/widgets/code/source_view.py +++ b/src/core/widgets/code/source_view.py @@ -10,14 +10,18 @@ from gi.repository import GLib from gi.repository import GtkSource # Application imports +from libs.dto.states import SourceViewStates + from .mixins.source_view_dnd_mixin import SourceViewDnDMixin class SourceView(GtkSource.View, SourceViewDnDMixin): - def __init__(self): + def __init__(self, state: SourceViewStates = SourceViewStates.INSERT): super(SourceView, self).__init__() + self.state = state + self._cut_temp_timeout_id = None self._cut_buffer = "" diff --git a/src/core/widgets/webkit/webkit_ui.py b/src/core/widgets/webkit/webkit_ui.py index d6476c2..749a717 100644 --- a/src/core/widgets/webkit/webkit_ui.py +++ b/src/core/widgets/webkit/webkit_ui.py @@ -44,6 +44,7 @@ class WebkitUI(WebKit2.WebView): data = f.read() self.load_html(content = data, base_uri = f"file://{path}/") + # self.load_uri("https://duckduckgo.com/") def _setup_content_manager(self): content_manager = self.get_user_content_manager() diff --git a/src/libs/controllers/controller_base.py b/src/libs/controllers/controller_base.py index fe2e5cd..88d564d 100644 --- a/src/libs/controllers/controller_base.py +++ b/src/libs/controllers/controller_base.py @@ -3,7 +3,7 @@ # Lib imports # Application imports -from libs.singleton import Singleton +from ..singleton import Singleton from ..dto.base_event import BaseEvent @@ -25,7 +25,7 @@ class ControllerBase(Singleton, EmitDispatcher): def _controller_message(self, event: BaseEvent): - raise ControllerBaseException("Controller Base must override '_controller_message'...") + raise ControllerBaseException("Controller Base '_controller_message' must be overridden...") def set_controller_context(self, controller_context: ControllerContext): self.controller_context = controller_context diff --git a/src/libs/controllers/controller_manager.py b/src/libs/controllers/controller_manager.py index e7d010f..1288b2b 100644 --- a/src/libs/controllers/controller_manager.py +++ b/src/libs/controllers/controller_manager.py @@ -24,7 +24,7 @@ class ControllerManager(Singleton, dict): def _crete_controller_context(self) -> ControllerContext: controller_context = ControllerContext() controller_context.message_to = self.message_to - controller_context.message = self.message + controller_context.message = self.message return controller_context diff --git a/src/libs/dto/code/__init__.py b/src/libs/dto/code/__init__.py index c3c48a4..b814883 100644 --- a/src/libs/dto/code/__init__.py +++ b/src/libs/dto/code/__init__.py @@ -4,6 +4,8 @@ from .code_event import CodeEvent +from .register_provider_event import RegisterProviderEvent + from .get_command_system_event import GetCommandSystemEvent from .request_completion_event import RequestCompletionEvent from .cursor_moved_event import CursorMovedEvent @@ -18,6 +20,7 @@ from .added_new_file_event import AddedNewFileEvent from .swapped_file_event import SwappedFileEvent from .popped_file_event import PoppedFileEvent from .removed_file_event import RemovedFileEvent +from .saved_file_event import SavedFileEvent from .get_file_event import GetFileEvent from .get_swap_file_event import GetSwapFileEvent diff --git a/src/libs/dto/code/register_provider_event.py b/src/libs/dto/code/register_provider_event.py new file mode 100644 index 0000000..b0a0b38 --- /dev/null +++ b/src/libs/dto/code/register_provider_event.py @@ -0,0 +1,19 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports +import gi +gi.require_version('GtkSource', '4') + +from gi.repository import GtkSource + +# Application imports +from ..base_event import BaseEvent + + + +@dataclass +class RegisterProviderEvent(BaseEvent): + provider_name: str = "" + provider: GtkSource.CompletionProvider = None + language_ids: list = field(default_factory=lambda: []) diff --git a/src/libs/dto/code/saved_file_event.py b/src/libs/dto/code/saved_file_event.py new file mode 100644 index 0000000..42636ec --- /dev/null +++ b/src/libs/dto/code/saved_file_event.py @@ -0,0 +1,13 @@ +# Python imports +from dataclasses import dataclass, field + +# Lib imports + +# Application imports +from .code_event import CodeEvent + + + +@dataclass +class SavedFileEvent(CodeEvent): + ... diff --git a/src/libs/dto/code/text_inserted_event.py b/src/libs/dto/code/text_inserted_event.py index 30f5f2e..8bdbe96 100644 --- a/src/libs/dto/code/text_inserted_event.py +++ b/src/libs/dto/code/text_inserted_event.py @@ -2,6 +2,11 @@ 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 @@ -10,6 +15,6 @@ from .code_event import CodeEvent @dataclass class TextInsertedEvent(CodeEvent): - line: int = 0 - char: int = 0 - value: str = "" + location: Gtk.TextIter = None + text: str = "" + length: int = 0 diff --git a/src/libs/dto/states/__init__.py b/src/libs/dto/states/__init__.py new file mode 100644 index 0000000..1f05351 --- /dev/null +++ b/src/libs/dto/states/__init__.py @@ -0,0 +1,7 @@ +""" + Code DTO States Package +""" + +from .source_view_states import SourceViewStates +from .cursor_action import CursorAction +from .move_direction import MoveDirection diff --git a/src/libs/dto/states/cursor_action.py b/src/libs/dto/states/cursor_action.py new file mode 100644 index 0000000..810561d --- /dev/null +++ b/src/libs/dto/states/cursor_action.py @@ -0,0 +1,14 @@ +# Python imports +from enum import Enum + +# Lib imports + +# Application imports + + + +class CursorAction(Enum): + NONE = 0 + DELETE = 1 + BACKSPACE = 2 + ENTER = 3 diff --git a/src/libs/dto/states/move_direction.py b/src/libs/dto/states/move_direction.py new file mode 100644 index 0000000..9ef314a --- /dev/null +++ b/src/libs/dto/states/move_direction.py @@ -0,0 +1,15 @@ +# Python imports +from enum import Enum + +# Lib imports + +# Application imports + + + +class MoveDirection(Enum): + NONE = 0 + UP = 1 + DOWN = 2 + LEFT = 3 + RIGHT = 4 diff --git a/src/libs/dto/states/source_view_states.py b/src/libs/dto/states/source_view_states.py new file mode 100644 index 0000000..b1c3c69 --- /dev/null +++ b/src/libs/dto/states/source_view_states.py @@ -0,0 +1,14 @@ +# Python imports +from enum import Enum + +# Lib imports + +# Application imports + + + +class SourceViewStates(Enum): + INSERT = 0 + MULTIINSERT = 1 + COMMAND = 2 + READONLY = 3 diff --git a/src/libs/settings/other/webkit_ui_settings.py b/src/libs/settings/other/webkit_ui_settings.py index 981ea49..955860c 100644 --- a/src/libs/settings/other/webkit_ui_settings.py +++ b/src/libs/settings/other/webkit_ui_settings.py @@ -39,4 +39,5 @@ class WebkitUISettings(WebKit2.Settings): self.set_enable_webaudio(True) self.set_enable_accelerated_2d_canvas(True) - self.set_user_agent(f"{APP_NAME}") \ No newline at end of file + self.set_user_agent(f"Mozilla/5.0 (macOS, AArch64) {APP_NAME}/1.0 Chrome/140.0.0 AppleWebKit/537.36 Safari/537.36") + diff --git a/src/plugins/controller.py b/src/plugins/controller.py index 588798c..ead0ebf 100644 --- a/src/plugins/controller.py +++ b/src/plugins/controller.py @@ -55,7 +55,7 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi for file in os.listdir(path): _path = os.path.join(path, file) if os.path.isdir(_path): - self.collect_search_locations(_path, locations) + self._collect_search_locations(_path, locations) def _load_plugins( self, @@ -70,7 +70,7 @@ class PluginsController(ControllerBase, PluginsControllerMixin, PluginReloadMixi try: target = join(path, "plugin.py") if not os.path.exists(target): - raise InvalidPluginException("Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load...") + raise PluginsControllerException("Invalid Plugin Structure: Plugin doesn't have 'plugin.py'. Aboarting load...") module = self._load_plugin_module(path, folder, target) diff --git a/src/plugins/manifest_manager.py b/src/plugins/manifest_manager.py index f065c0f..e41b92d 100644 --- a/src/plugins/manifest_manager.py +++ b/src/plugins/manifest_manager.py @@ -32,11 +32,8 @@ class ManifestManager: for path, folder in [ [join(self._plugins_path, item), item] - if - os.path.isdir( join(self._plugins_path, item) ) - else - None for item in os.listdir(self._plugins_path) + if os.path.isdir( join(self._plugins_path, item) ) ]: self.load(folder, path) diff --git a/src/plugins/plugin_types/__init__.py b/src/plugins/plugin_types/__init__.py new file mode 100644 index 0000000..5814687 --- /dev/null +++ b/src/plugins/plugin_types/__init__.py @@ -0,0 +1,7 @@ +""" + Plugin Types Module +""" + +from .plugin_base import PluginBase +from .plugin_ui import PluginUI +from .plugin_code import PluginCode diff --git a/src/plugins/plugin_types/plugin_base.py b/src/plugins/plugin_types/plugin_base.py new file mode 100644 index 0000000..1b1b97a --- /dev/null +++ b/src/plugins/plugin_types/plugin_base.py @@ -0,0 +1,49 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.dto.base_event import BaseEvent + +from ..plugin_context import PluginContext + + + +class PluginBaseException(Exception): + ... + + + +class PluginBase: + def __init__(self, *args, **kwargs): + super(PluginBase, self).__init__(*args, **kwargs) + + self.plugin_context: PluginContext = None + + + def _controller_message(self, event: BaseEvent): + raise PluginBaseException("Plugin Base '_controller_message' must be overriden by Plugin") + + def load(self): + raise PluginBaseException("Plugin Base 'load' must be overriden by Plugin") + + def run(self): + raise PluginBaseException("Plugin Base 'run' must be overriden by Plugin") + + def requests_ui_element(self, element_id: str): + raise PluginBaseException("Plugin Base 'requests_ui_element' must be overriden by Plugin") + + def message(self, event: BaseEvent): + raise PluginBaseException("Plugin Base 'message' must be overriden by Plugin") + + def message_to(self, name: str, event: BaseEvent): + raise PluginBaseException("Plugin Base 'message_to' must be overriden by Plugin") + + def message_to_selected(self, names: list[str], event: BaseEvent): + raise PluginBaseException("Plugin Base 'message_to_selected' must be overriden by Plugin") + + def emit(self, event_type: str, data: tuple = ()): + raise PluginBaseException("Plugin Base 'emit' must be overriden by Plugin") + + def emit_and_await(self, event_type: str, data: tuple = ()): + raise PluginBaseException("Plugin Base 'emit_and_await' must be overriden by Plugin") diff --git a/src/plugins/plugin_types/plugin_code.py b/src/plugins/plugin_types/plugin_code.py new file mode 100644 index 0000000..37438c1 --- /dev/null +++ b/src/plugins/plugin_types/plugin_code.py @@ -0,0 +1,41 @@ +# Python imports + +# Lib imports + +# Application imports +from libs.dto.base_event import BaseEvent + +from ..plugin_context import PluginContext +from .plugin_base import PluginBase + + + +class PluginCodeException(Exception): + ... + + + +class PluginCode(PluginBase): + def __init__(self, *args, **kwargs): + super(PluginCode, self).__init__(*args, **kwargs) + + self.plugin_context: PluginContext = None + + + def _controller_message(self, event: BaseEvent): + raise PluginCodeException("Plugin Code '_controller_message' must be overriden by Plugin") + + def load(self): + raise PluginCodeException("Plugin Code 'load' must be overriden by Plugin") + + def run(self): + raise PluginCodeException("Plugin Code 'run' must be overriden by Plugin") + + def message(self, event: BaseEvent): + return self.plugin_context.message(event) + + def message_to(self, name: str, event: BaseEvent): + return self.plugin_context.message_to(name, event) + + def message_to_selected(self, names: list[str], event: BaseEvent): + return self.plugin_context.message_to_selected(names, event) diff --git a/src/plugins/plugin_base.py b/src/plugins/plugin_types/plugin_ui.py similarity index 68% rename from src/plugins/plugin_base.py rename to src/plugins/plugin_types/plugin_ui.py index 08a1ccc..f5e3534 100644 --- a/src/plugins/plugin_base.py +++ b/src/plugins/plugin_types/plugin_ui.py @@ -5,30 +5,31 @@ # Application imports from libs.dto.base_event import BaseEvent -from .plugin_context import PluginContext +from ..plugin_context import PluginContext +from .plugin_base import PluginBase -class PluginBaseException(Exception): +class PluginCodeException(Exception): ... -class PluginBase: +class PluginUI(PluginBase): def __init__(self, *args, **kwargs): - super(PluginBase, self).__init__(*args, **kwargs) + super(PluginUI, self).__init__(*args, **kwargs) self.plugin_context: PluginContext = None def _controller_message(self, event: BaseEvent): - raise PluginBaseException("Plugin Base '_controller_message' must be overriden by Plugin") + raise PluginCodeException("Plugin UI '_controller_message' must be overriden by Plugin") def load(self): - raise PluginBaseException("Plugin Base 'load' must be overriden by Plugin") + raise PluginCodeException("Plugin UI 'load' must be overriden by Plugin") def run(self): - raise PluginBaseException("Plugin Base 'run' must be overriden by Plugin") + raise PluginCodeException("Plugin UI 'run' must be overriden by Plugin") def requests_ui_element(self, element_id: str): return self.plugin_context.requests_ui_element(element_id) diff --git a/src/plugins/plugins_controller_mixin.py b/src/plugins/plugins_controller_mixin.py index 0691525..3583ee3 100644 --- a/src/plugins/plugins_controller_mixin.py +++ b/src/plugins/plugins_controller_mixin.py @@ -6,6 +6,10 @@ +class InvalidPluginException(Exception): + ... + + class PluginsControllerMixin: