initial commit

This commit is contained in:
2026-01-04 11:42:27 -06:00
commit b79220c1ae
1930 changed files with 37547 additions and 0 deletions

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
{
"name": "Search/Replace",
"author": "ITDominator",
"version": "0.0.1",
"support": "",
"requests": {
"pass_events": true,
"pass_ui_objects": ["separator_botton"],
"bind_keys": ["Search/Replace||tggl_search_replace:<Control>f"]
}
}

View File

@@ -0,0 +1,221 @@
# Python imports
import os
import re
import threading
# 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
from gi.repository import GLib
# Application imports
from plugins.plugin_base import PluginBase
from .styling_mixin import StylingMixin
from .replace_mixin import ReplaceMixin
class Plugin(StylingMixin, ReplaceMixin, PluginBase):
def __init__(self):
super().__init__()
self.name = "Search/Replace" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
# where self.name should not be needed for message comms
self.path = os.path.dirname(os.path.realpath(__file__))
self._GLADE_FILE = f"{self.path}/search_replace.glade"
self._search_replace_dialog = None
self._find_entry = None
self._replace_entry = None
self._active_src_view = None
self._buffer = None
self._tag_table = None
self.use_regex = False
self.use_case_sensitive = False
self.search_only_in_selection = False
self.use_whole_word_search = False
self.timer = None
self.search_time = 0.35
self.find_text = ""
self.search_tag = "search_tag"
self.highlight_color = "#FBF719"
self.text_color = "#000000"
self.alpha_num_under = re.compile(r"[a-zA-Z0-9_]")
def run(self):
self._builder = Gtk.Builder()
self._builder.add_from_file(self._GLADE_FILE)
self._connect_builder_signals(self, self._builder)
separator_botton = self._ui_objects[0]
self._search_replace_dialog = self._builder.get_object("search_replace_dialog")
self._find_status_lbl = self._builder.get_object("find_status_lbl")
self._find_options_lbl = self._builder.get_object("find_options_lbl")
self._find_entry = self._builder.get_object("find_entry")
self._replace_entry = self._builder.get_object("replace_entry")
self._search_replace_dialog.set_relative_to(separator_botton)
self._search_replace_dialog.set_hexpand(True)
def generate_reference_ui_element(self):
...
def subscribe_to_events(self):
self._event_system.subscribe("tggl_search_replace", self._tggl_search_replace)
self._event_system.subscribe("set_active_src_view", self._set_active_src_view)
def _set_active_src_view(self, source_view):
self._active_src_view = source_view
self._buffer = self._active_src_view.get_buffer()
self._tag_table = self._buffer.get_tag_table()
self.search_for_string(self._find_entry)
def _show_search_replace(self, widget = None, eve = None):
self._search_replace_dialog.popup()
def _tggl_search_replace(self, widget = None, eve = None):
is_visible = self._search_replace_dialog.is_visible()
buffer = self._active_src_view.get_buffer()
data = None
if buffer.get_has_selection():
start, end = buffer.get_selection_bounds()
data = buffer.get_text(start, end, include_hidden_chars = False)
if data:
self._find_entry.set_text(data)
if not is_visible:
self._search_replace_dialog.popup();
self._find_entry.grab_focus()
elif not data and is_visible:
self._search_replace_dialog.popdown()
self._find_entry.set_text("")
else:
self._find_entry.grab_focus()
def get_search_tag(self, buffer):
tag_table = buffer.get_tag_table()
search_tag = tag_table.lookup(self.search_tag)
if not search_tag:
search_tag = buffer.create_tag(self.search_tag, background = self.highlight_color, foreground = self.text_color)
buffer.remove_tag_by_name(self.search_tag, buffer.get_start_iter(), buffer.get_end_iter())
return search_tag
def cancel_timer(self):
if self.timer:
self.timer.cancel()
GLib.idle_remove_by_data(None)
def delay_search_glib(self):
GLib.idle_add(self._do_highlight)
def delay_search(self):
wait_time = self.search_time / len(self.find_text)
wait_time = max(wait_time, 0.05)
self.timer = threading.Timer(wait_time, self.delay_search_glib)
self.timer.daemon = True
self.timer.start()
def on_enter_search(self, widget, eve):
text = widget.get_text()
if not text: return
keyname = Gdk.keyval_name(eve.keyval)
if keyname == "Return":
self.find_next(widget)
def search_for_string(self, widget):
self.cancel_timer()
self.find_text = widget.get_text()
if len(self.find_text) > 0 and len(self.find_text) < 5:
self.delay_search()
else:
self._do_highlight(self.find_text)
def _do_highlight(self, query = None):
query = self.find_text if not query else query
buffer = self._active_src_view.get_buffer()
# Also clears tag from buffer so if no query we're clean in ui
search_tag = self.get_search_tag(buffer)
self.update_style(1)
if not query:
self._find_status_lbl.set_label(f"Find in current buffer")
self.update_style(0)
return
start_itr = buffer.get_start_iter()
end_itr = buffer.get_end_iter()
results, total_count = self.search(start_itr, query)
self._update_status_lbl(total_count, query)
for start, end in results:
buffer.apply_tag(search_tag, start, end)
def search(self, start_itr = None, query = None, limit = None):
if not start_itr or not query: return None, None
flags = Gtk.TextSearchFlags.VISIBLE_ONLY | Gtk.TextSearchFlags.TEXT_ONLY
if not self.use_case_sensitive:
flags = flags | Gtk.TextSearchFlags.CASE_INSENSITIVE
if self.search_only_in_selection and self._buffer.get_has_selection():
start_itr, limit = self._buffer.get_selection_bounds()
_results = []
while True:
result = start_itr.forward_search(query, flags, limit)
if not result: break
_results.append(result)
start_itr = result[1]
results = self.apply_filters(_results, query)
return results, len(results)
def apply_filters(self, _results, query):
results = []
for start, end in _results:
text = self._buffer.get_slice(start, end, include_hidden_chars = False)
if self.use_whole_word_search:
if not self.is_whole_word(start, end):
continue
results.append([start, end])
return results
def find_next(self, widget, eve = None, use_data = None):
mark = self._buffer.get_insert()
iter = self._buffer.get_iter_at_mark(mark)
iter.forward_line()
search_tag = self._tag_table.lookup(self.search_tag)
next_tag_found = iter.forward_to_tag_toggle(search_tag)
if not next_tag_found:
self._buffer.place_cursor( self._buffer.get_start_iter() )
mark = self._buffer.get_insert()
iter = self._buffer.get_iter_at_mark(mark)
iter.forward_to_tag_toggle(search_tag)
self._buffer.place_cursor(iter)
self._active_src_view.scroll_to_mark( self._buffer.get_insert(), 0.0, True, 0.0, 0.0 )
def find_all(self, widget):
...

View File

@@ -0,0 +1,94 @@
# Python imports
# Lib imports
# Application imports
class ReplaceMixin:
def replace(self, widget):
replace_text = self._replace_entry.get_text()
if self.find_text and replace_text:
self._buffer.begin_user_action()
iter = self._buffer.get_start_iter()
search_tag = self._tag_table.lookup(self.search_tag)
iter.forward_to_tag_toggle(search_tag)
self._do_replace(iter, replace_text)
self._active_src_view.scroll_to_iter( iter, 0.0, True, 0.0, 0.0 )
self._buffer.end_user_action()
def replace_all(self, widget):
replace_text = self._replace_entry.get_text()
if self.find_text:
self._buffer.begin_user_action()
mark = self._buffer.get_insert()
iter = self._buffer.get_start_iter()
search_tag = self._tag_table.lookup(self.search_tag)
while iter.forward_to_tag_toggle(search_tag):
self._do_replace(iter, replace_text)
iter = self._buffer.get_start_iter()
self._buffer.end_user_action()
def _do_replace(self, iter, text):
start, end = self.get_start_end(iter)
self.replace_in_buffer(start, end, text)
def replace_in_buffer(self, start, end, text):
pos_mark = self._buffer.create_mark("find-replace", end, True)
self._buffer.delete(start, end)
replace_iter = self._buffer.get_iter_at_mark(pos_mark)
self._buffer.insert(replace_iter, text)
def get_start_end(self, iter):
start = iter.copy()
end = None
while True:
iter.forward_char()
tags = iter.get_tags()
valid = False
for tag in tags:
if tag.props.name and self.search_tag in tag.props.name:
valid = True
break
if valid:
continue
end = iter.copy()
break
return start, end
# NOTE: Below, lovingly taken from Hamad Al Marri's Gamma text editor.
# Link: https://gitlab.com/hamadmarri/gamma-text-editor
def is_whole_word(self, match_start, match_end):
is_prev_a_char = True
is_next_a_char = True
prev_iter = match_start.copy()
next_iter = match_end.copy()
if not prev_iter.backward_char():
is_prev_a_char = False
else:
c = prev_iter.get_char()
is_prev_a_char = (c.isalpha() or c.isdigit())
if not next_iter:
is_next_a_char = False
else:
c = next_iter.get_char()
is_next_a_char = (c.isalpha() or c.isdigit())
is_word = (not is_prev_a_char and not is_next_a_char)
# Note: Both must be false to be a word...
return is_word

View File

@@ -0,0 +1,299 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkImage" id="close_img">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stock">gtk-close</property>
</object>
<object class="GtkImage" id="only-in-selection">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixbuf">../../icons/only-in-selection.png</property>
</object>
<object class="GtkImage" id="whole-word">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="pixbuf">../../icons/whole-word.png</property>
</object>
<object class="GtkPopover" id="search_replace_dialog">
<property name="can-focus">False</property>
<property name="modal">False</property>
<property name="transitions-enabled">False</property>
<property name="constrain-to">none</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="find_status_lbl">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="label" translatable="yes">Find in Current Buffer</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="find_options_lbl">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="xpad">20</property>
<property name="label" translatable="yes">Finding with Options: Case Insensitive</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">start</property>
<child>
<object class="GtkToggleButton">
<property name="label" translatable="yes">.*</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Use Regex</property>
<signal name="toggled" handler="tggle_regex" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkToggleButton">
<property name="label" translatable="yes">Aa</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Match Case</property>
<signal name="toggled" handler="tggle_case_sensitive" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Only In Selection</property>
<property name="image">only-in-selection</property>
<property name="always-show-image">True</property>
<signal name="toggled" handler="tggle_selection_only_scan" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkToggleButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Whole Word</property>
<property name="image">whole-word</property>
<property name="always-show-image">True</property>
<signal name="toggled" handler="tggle_whole_word_search" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Close Panel</property>
<property name="image">close_img</property>
<property name="always-show-image">True</property>
<signal name="clicked" handler="_tggl_search_replace" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<!-- n-columns=10 n-rows=2 -->
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="column-homogeneous">True</property>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Replace All</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Replace All</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<signal name="clicked" handler="replace_all" swapped="no"/>
</object>
<packing>
<property name="left-attach">9</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Replace</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Replace Next</property>
<property name="margin-start">5</property>
<property name="margin-end">10</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<signal name="clicked" handler="replace" swapped="no"/>
</object>
<packing>
<property name="left-attach">8</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Find All</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<signal name="clicked" handler="find_all" swapped="no"/>
</object>
<packing>
<property name="left-attach">9</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">Find</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<signal name="clicked" handler="find_next" swapped="no"/>
</object>
<packing>
<property name="left-attach">8</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkSearchEntry" id="find_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Find in current buffer</property>
<signal name="key-release-event" handler="on_enter_search" swapped="no"/>
<signal name="search-changed" handler="search_for_string" swapped="no"/>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
<property name="width">8</property>
</packing>
</child>
<child>
<object class="GtkSearchEntry" id="replace_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<property name="placeholder-text" translatable="yes">Replace in current buffer</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
<property name="width">8</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">10</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -0,0 +1,66 @@
# Python imports
# Lib imports
# Application imports
class StylingMixin:
def tggle_regex(self, widget):
self.use_regex = not widget.get_active()
self._set_find_options_lbl()
self.search_for_string(self._find_entry)
def tggle_case_sensitive(self, widget):
self.use_case_sensitive = widget.get_active()
self._set_find_options_lbl()
self.search_for_string(self._find_entry)
def tggle_selection_only_scan(self, widget):
self.search_only_in_selection = widget.get_active()
self._set_find_options_lbl()
self.search_for_string(self._find_entry)
def tggle_whole_word_search(self, widget):
self.use_whole_word_search = widget.get_active()
self._set_find_options_lbl()
self.search_for_string(self._find_entry)
def _set_find_options_lbl(self):
find_options = "Finding with Options: "
if self.use_regex:
find_options += "Regex"
find_options += ", " if self.use_regex else ""
find_options += "Case Sensitive" if self.use_case_sensitive else "Case Inensitive"
if self.search_only_in_selection:
find_options += ", Within Current Selection"
if self.use_whole_word_search:
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 ""
if total_count == 0: self.update_style(2)
self._find_status_lbl.set_label(f"{count} result{plural} found for '{query}'")