Moved plugins and refactor command system
- Moved plugins to apropriate sub folders - Refactor command system with new add_command method and rename GetCommandSystemEvent to GetNewCommandSystemEvent - Add RegisterCommandEvent for dynamic command registration - Change footer container orientation to VERTICAL - Add search-highlight tag to source buffer - Add file change detection (deleted, externally modified) in source_file - Add JSON prettify option to source view popup menu - Enable hexpand on VTE widget - Update plugins_controller_mixin to use widget_registry
This commit is contained in:
3
plugins/code/search_replace/__init__.py
Normal file
3
plugins/code/search_replace/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
||||
3
plugins/code/search_replace/__main__.py
Normal file
3
plugins/code/search_replace/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
||||
BIN
plugins/code/search_replace/images/only-in-selection.png
Normal file
BIN
plugins/code/search_replace/images/only-in-selection.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
plugins/code/search_replace/images/whole-word.png
Normal file
BIN
plugins/code/search_replace/images/whole-word.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
7
plugins/code/search_replace/manifest.json
Normal file
7
plugins/code/search_replace/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Search/Replace",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {}
|
||||
}
|
||||
3
plugins/code/search_replace/mixins/__init__.py
Normal file
3
plugins/code/search_replace/mixins/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Pligin Module Mixins
|
||||
"""
|
||||
50
plugins/code/search_replace/mixins/replace_mixin.py
Normal file
50
plugins/code/search_replace/mixins/replace_mixin.py
Normal 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()
|
||||
79
plugins/code/search_replace/mixins/search_mixin.py
Normal file
79
plugins/code/search_replace/mixins/search_mixin.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# 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
|
||||
|
||||
# if end_itr.forward_char():
|
||||
# next_char = end_itr.get_char()
|
||||
# if self.is_word_char(next_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.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)
|
||||
88
plugins/code/search_replace/mixins/search_replace_mixin.py
Normal file
88
plugins/code/search_replace/mixins/search_replace_mixin.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# 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.clear_highlight(buffer)
|
||||
return
|
||||
|
||||
self._find_all_matches(search_text, buffer)
|
||||
self._highlight_all_matches(buffer)
|
||||
|
||||
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)
|
||||
75
plugins/code/search_replace/mode_buttons.py
Normal file
75
plugins/code/search_replace/mode_buttons.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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()
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
|
||||
65
plugins/code/search_replace/plugin.py
Normal file
65
plugins/code/search_replace/plugin.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# 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 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.requests_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"
|
||||
)
|
||||
|
||||
self.message_to("source_views", event)
|
||||
|
||||
def run(self):
|
||||
...
|
||||
|
||||
|
||||
class Handler:
|
||||
@staticmethod
|
||||
def execute(
|
||||
view: any
|
||||
):
|
||||
logger.debug("Command: Search/Replace")
|
||||
search_replace.hide() if search_replace.is_visible() else search_replace.show()
|
||||
152
plugins/code/search_replace/search_replace.py
Normal file
152
plugins/code/search_replace/search_replace.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# 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 = None
|
||||
self.matches: list[tuple] = []
|
||||
|
||||
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_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.stateus_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.stateus_lbl, left = 0, top = 0, width = 2, height = 1)
|
||||
self.attach(child = self.find_options_lbl, left = 2, top = 0, width = 2, height = 1)
|
||||
self.attach(child = self.mode_bttn_box, left = 4, top = 0, width = 2, height = 1)
|
||||
|
||||
self.attach(child = self.find_entry, left = 0, top = 1, width = 4, height = 1)
|
||||
self.attach(child = find_bttn, left = 4, top = 1, width = 1, height = 1)
|
||||
self.attach(child = find_all_bttn, left = 5, top = 1, width = 1, height = 1)
|
||||
|
||||
self.attach(child = self.replace_entry, left = 0, top = 2, width = 4, height = 1)
|
||||
self.attach(child = replace_bttn, left = 4, top = 2, width = 1, height = 1)
|
||||
self.attach(child = replace_all_bttn, left = 5, top = 2, width = 1, height = 1)
|
||||
|
||||
def _handle_show(self, widget):
|
||||
self.find_entry.set_text("")
|
||||
self.find_entry.grab_focus()
|
||||
|
||||
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 request_update(self):
|
||||
self._find_entry_search_change(self.find_entry)
|
||||
|
||||
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 clear_highlight(self, buffer):
|
||||
if not self.highlight_tag: return
|
||||
start, end = buffer.get_bounds()
|
||||
buffer.remove_tag(self.highlight_tag, start, end)
|
||||
|
||||
Reference in New Issue
Block a user