Clean up codebase and improve file loading

- Moved plugins to proper sub groups (autopairs, code_minimap, colorize, commentzar, info_bar, markdown_preview, prettify_json, search_replace, tabs_bar, telescope, toggle_source_view, lsp_client)
- Add filter_out_loaded_files to prevent opening already-loaded files
- Add INDEPENDENT source view state
- Fix cursor scroll position on buffer switch
- Fix signal blocking during file load
- Fix word boundary in completion provider
- Refactor code events into single events module
This commit is contained in:
2026-03-08 00:51:28 -06:00
parent a52d5243ab
commit 99dc917de3
229 changed files with 8809 additions and 756 deletions

View File

@@ -0,0 +1,3 @@
"""
Pligin Module
"""

View File

@@ -0,0 +1,3 @@
"""
Pligin Package
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,7 @@
{
"name": "Search/Replace",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {}
}

View File

@@ -0,0 +1,3 @@
"""
Pligin Module Mixins
"""

View File

@@ -0,0 +1,50 @@
# Python imports
# Lib imports
# Application imports
class ReplaceMixin:
def _replace_word(
self,
current_index: int,
to_text: str,
buffer: any
):
self.clear_highlight(buffer)
start_itr, end_itr = self.matches[current_index]
self.active_view.scroll_to_iter(end_itr, 0.2, False, 0, 0)
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, to_text)
buffer.end_user_action()
def _replace_all_words(self, to_text: str, buffer: any):
marks: list = []
for start_itr, end_itr in self.matches:
start_mark = buffer.create_mark(None, start_itr, left_gravity = True)
end_mark = buffer.create_mark(None, end_itr, left_gravity = False)
marks.append((start_mark, end_mark))
buffer.begin_user_action()
for start_mark, end_mark in reversed(marks):
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, to_text)
buffer.end_user_action()
for start_mark, end_mark in marks:
buffer.delete_mark(start_mark)
buffer.delete_mark(end_mark)
self.find_entry.grab_focus()

View File

@@ -0,0 +1,77 @@
# Python imports
from contextlib import suppress
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class SearchMixin:
def is_word_char(self, ch):
return ch.isalnum() or ch == "_"
def is_whole_word(self, start_itr, end_itr, buffer):
if start_itr.backward_char():
prev_char = start_itr.get_char()
if self.is_word_char(prev_char):
return False
next_char = end_itr.get_char()
if self.is_word_char(next_char):
return False
return True
def _find_all_matches(self, search_text, buffer):
self.update_style(0)
self.matches.clear()
self.current_index = -1
start_itr = buffer.get_start_iter()
end_itr = buffer.get_end_iter()
case_mode = Gtk.TextSearchFlags.CASE_INSENSITIVE if not self.mode_bttn_box.match_case else Gtk.TextSearchFlags.TEXT_ONLY
whole_word = self.mode_bttn_box.whole_word
if self.mode_bttn_box.in_selection:
with suppress(Exception):
start_itr, end_itr = buffer.get_selection_bounds()
while True:
match = start_itr.forward_search(
search_text,
case_mode,
end_itr
)
if not match: break
match_start, match_end = match
if whole_word and not self.is_whole_word(match_start.copy(), match_end.copy(), buffer):
start_itr = match_end
continue
self.matches.append(
(match_start.copy(), match_end.copy())
)
start_itr = match_end
def _highlight_all_matches(self, buffer):
self.clear_highlight(buffer)
for start_itr, end_itr in self.matches:
buffer.apply_tag(self.highlight_tag, start_itr, end_itr)
def _search_for_next_word(self, buffer):
self.current_index = (self.current_index + 1) % len(self.matches)
self._highlight_current(self.current_index, buffer)
def _search_for_prev_word(self, buffer):
self.current_index = (self.current_index - 1) % len(self.matches)
self._highlight_current(self.current_index, buffer)

View File

@@ -0,0 +1,91 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
# Application imports
from .search_mixin import SearchMixin
from .replace_mixin import ReplaceMixin
class SearchReplaceMixin(SearchMixin, ReplaceMixin):
def _find_entry_focus_in_event(self, entry, event):
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()
entry.set_text(
buffer.get_text(
start_itr,
end_itr,
include_hidden_chars = False
)
)
return
def _find_entry_search_change(self, entry):
search_text = entry.get_text()
buffer = self.active_view.get_buffer()
self.highlight_tag = buffer.get_tag_table().lookup("search-highlight")
if not search_text:
self.update_style(-1)
self.clear_highlight(buffer)
self.status_lbl.set_label("Find in current buffer...")
return
self._find_all_matches(search_text, buffer)
self._highlight_all_matches(buffer)
self._update_status_lbl(len(self.matches), search_text)
def _find_entry_activate(self, entry):
self._find_entry_next_match(entry)
def _find_entry_next_match(self, entry):
search_text = entry.get_text()
if not search_text: return
buffer = self.active_view.get_buffer()
self._search_for_next_word(buffer)
def _find_entry_previous_match(self, entry):
search_text = entry.get_text()
if not search_text: return
buffer = self.active_view.get_buffer()
self._search_for_prev_word(buffer)
def _replace_entry_activate(self, entry):
to_text = entry.get_text()
if not to_text: return
buffer = self.active_view.get_buffer()
self._replace_word(self.current_index, to_text, buffer)
self._find_entry_search_change(self.find_entry)
def _replace_all_activate(self, entry):
to_text = entry.get_text()
if not to_text: return
buffer = self.active_view.get_buffer()
self._replace_all_words(to_text, buffer)
def _highlight_current(self, current_index, buffer):
self.clear_highlight(buffer)
start_itr, end_itr = self.matches[current_index]
buffer.apply_tag(self.highlight_tag, start_itr, end_itr)
self.active_view.scroll_to_iter(end_itr, 0.2, False, 0, 0)

View File

@@ -0,0 +1,82 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
class ModeException(Exception):
...
class ModeButtons(Gtk.ButtonBox):
def __init__(self):
super(ModeButtons, self).__init__()
self.use_regex: bool = False
self.match_case: bool = False
self.in_selection: bool = False
self.whole_word: bool = False
self._setup_styling()
self._setup_signals()
self._load_widgets()
def _setup_styling(self):
ctx = self.get_style_context()
ctx.add_class("search-replace-mode-buttons")
def _setup_signals(self):
...
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")
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")
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")
hide_bttn.connect(
"clicked",
lambda widget: self.get_parent().hide()
)
in_selection_bttn.set_image(
Gtk.Image.new_from_file("images/only-in-selection.png")
)
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)
def _toggled_button(self, toggle_button, mode: str):
setattr(self, mode, not getattr(self, mode))
self.request_update()
def request_update(self):
raise ModeException("Must by 'monkey' patched from search_replace.py")

View File

@@ -0,0 +1,66 @@
# Python imports
# Lib imports
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from plugins.plugin_types import PluginCode
from .search_replace import SearchReplace
search_replace = SearchReplace()
class Plugin(PluginCode):
def __init__(self):
super(Plugin, self).__init__()
def _controller_message(self, event: Code_Event_Types.CodeEvent):
if isinstance(event, Code_Event_Types.FocusedViewEvent):
self._handle_view_change(event)
def _handle_view_change(self, event: Code_Event_Types.FocusedViewEvent):
if search_replace.is_visible():
buffer = search_replace.active_view.get_buffer()
search_replace.clear_highlight(buffer)
search_replace.active_view = event.view
if search_replace.is_visible():
search_replace._find_entry_search_change(
search_replace.find_entry
)
def load(self):
footer = self.request_ui_element("footer-container")
footer.add( search_replace )
event = Event_Factory.create_event("register_command",
command_name = "search_replace",
command = Handler,
binding_mode = "released",
binding = ["<Control>f", "<Control>r"]
)
self.emit_to("source_views", event)
def run(self):
...
class Handler:
@staticmethod
def execute(
view: any,
*args,
**kwargs
):
logger.debug("Command: Search/Replace")
search_replace.last_key = args[0]
search_replace.hide() if search_replace.is_visible() else search_replace.show()

View File

@@ -0,0 +1,199 @@
# Python imports
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk
from gi.repository import Gdk
# Application imports
from .mixins.search_replace_mixin import SearchReplaceMixin
from .mode_buttons import ModeButtons
class SearchReplace(Gtk.Grid, SearchReplaceMixin):
def __init__(self):
super(SearchReplace, self).__init__()
self.active_view = None
self.highlight_tag: Gtk.TextTag = None
self.matches: list[tuple] = []
self.last_key: str = None
self._setup_styling()
self._setup_signals()
self._load_widgets()
self.show_all()
self.hide()
def _setup_styling(self):
ctx = self.get_style_context()
ctx.add_class("search-replace")
self.set_hexpand(True)
self.set_column_spacing(15)
self.set_row_spacing(15)
self.set_margin_start(15)
self.set_margin_end(15)
self.set_margin_top(15)
self.set_margin_bottom(15)
def _setup_signals(self):
self.connect("show", self._handle_show)
self.connect("hide", self._handle_hide)
def _load_widgets(self):
self.status_lbl = Gtk.Label(label = "Find in Current Buffer")
self.find_options_lbl = Gtk.Label(label = "Finding with Options: Case Insensitive")
self.mode_bttn_box = ModeButtons()
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_entry.set_hexpand(True)
self.replace_entry.set_hexpand(True)
self.find_entry.set_max_width_chars(16)
self.replace_entry.set_max_width_chars(16)
self.find_entry.set_placeholder_text("Find in current buffer...")
self.replace_entry.set_placeholder_text("Replace in current buffer...")
self.mode_bttn_box.request_update = self.request_update
self.find_entry.connect("focus-in-event", self._find_entry_focus_in_event)
self.find_entry.connect("key-release-event", self._find_entry_key_release_event)
self.find_entry.connect("activate", self._find_entry_activate)
self.find_entry.connect("search-changed", self._find_entry_search_change)
self.find_entry.connect("next-match", self._find_entry_next_match)
self.find_entry.connect("previous-match", self._find_entry_previous_match)
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(
"clicked",
lambda button: self._find_entry_next_match(self.find_entry)
)
find_all_bttn.connect(
"clicked",
lambda button: self._find_entry_search_change(self.find_entry)
)
replace_bttn.connect(
"clicked",
lambda button: self._replace_entry_activate(self.replace_entry)
)
replace_all_bttn.connect(
"clicked",
lambda button: self._replace_all_activate(self.replace_entry)
)
self.attach(child = self.status_lbl, left = 0, top = 0, width = 3, height = 1)
self.attach(child = self.find_options_lbl, left = 3, top = 0, width = 2, height = 1)
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.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)
def _handle_show(self, widget):
self.find_entry.set_text("")
if not self.last_key == "r":
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)
def _handle_hide(self, widget):
if not self.active_view: return
buffer = self.active_view.get_buffer()
self.clear_highlight(buffer)
self.active_view.grab_focus()
def _find_entry_key_release_event(self, widget, event):
modifiers = Gdk.ModifierType(event.get_state() & ~Gdk.ModifierType.LOCK_MASK)
is_control = True if modifiers & Gdk.ModifierType.CONTROL_MASK else False
is_shift = True if modifiers & Gdk.ModifierType.SHIFT_MASK else False
keyname = Gdk.keyval_name(event.keyval).lower()
if is_control and keyname == "f":
self.hide()
elif is_control and keyname == "r":
self.replace_entry.grab_focus()
def _replace_entry_key_release_event(self, widget, event):
modifiers = Gdk.ModifierType(event.get_state() & ~Gdk.ModifierType.LOCK_MASK)
is_control = True if modifiers & Gdk.ModifierType.CONTROL_MASK else False
is_shift = True if modifiers & Gdk.ModifierType.SHIFT_MASK else False
keyname = Gdk.keyval_name(event.keyval).lower()
if is_control and keyname == "l":
self.find_entry.grab_focus()
elif is_control and keyname == "f":
self.hide()
def _set_find_options_lbl(self):
find_options = "Finding with Options: "
if self.mode_bttn_box.use_regex:
find_options += "Regex"
find_options += ", " if self.mode_bttn_box.use_regex else ""
find_options += "Case Sensitive" if self.mode_bttn_box.match_case else "Case Insensitive"
if self.mode_bttn_box.in_selection:
find_options += ", Within Current Selection"
if self.mode_bttn_box.whole_word:
find_options += ", Whole Word"
self.find_options_lbl.set_label(find_options)
def update_style(self, state):
self.find_entry.get_style_context().remove_class("searching")
self.find_entry.get_style_context().remove_class("search-success")
self.find_entry.get_style_context().remove_class("search-fail")
if state == 0:
self.find_entry.get_style_context().add_class("searching")
elif state == 1:
self.find_entry.get_style_context().add_class("search-success")
elif state == 2:
self.find_entry.get_style_context().add_class("search-fail")
def _update_status_lbl(self, total_count: int = 0, query: str = None):
if not query: return
count = total_count if total_count > 0 else "No"
plural = "s" if total_count > 1 else ""
self.update_style(2) if total_count == 0 else self.update_style(1)
self.status_lbl.set_label(f"{count} result{plural} found for:\n'{query}'")
def request_update(self):
self._set_find_options_lbl()
self._find_entry_search_change(self.find_entry)
def clear_highlight(self, buffer):
if not self.highlight_tag: return
start_itr, end_itr = buffer.get_bounds()
buffer.remove_tag(self.highlight_tag, start_itr, end_itr)