Last active
August 6, 2026 05:51
-
-
Save acidnine/241516d5fb7bf8feb4dd9581ec190fb4 to your computer and use it in GitHub Desktop.
XED Plugin Selection Duplicate
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [Plugin] | |
| Loader=python3 | |
| Module=selection_duplicate | |
| IAge=3 | |
| Name=Duplicate Selected Text | |
| Description=Changes Ctrl+D to duplicate the selected text (if text is selected) instead of the entire line. | |
| Authors=Acidnine; Gemini / ChatGPT / Claude | |
| Copyright=Copyright © 2026 Acidnine | |
| Website=https://vault21.net/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import gi | |
| gi.require_version('Xed', '1.0') | |
| gi.require_version('Gtk', '3.0') | |
| from gi.repository import GObject, GLib, Xed, Gtk, Gdk | |
| class SelectionDuplicatePlugin(GObject.Object, Xed.WindowActivatable): | |
| __gtype_name__ = "SelectionDuplicatePlugin" | |
| window = GObject.Property(type=Xed.Window) | |
| def __init__(self): | |
| GObject.Object.__init__(self) | |
| self.handlers = {} | |
| self._binding_removed = False | |
| def do_activate(self): | |
| """Called when the plugin is activated in preferences.""" | |
| self.handlers[self.window] = [ | |
| self.window.connect("tab-added", self.on_tab_added), | |
| self.window.connect("active-tab-changed", self.on_active_tab_changed) | |
| ] | |
| view = self.window.get_active_view() | |
| if view: | |
| self.setup_view(view) | |
| def _strip_native_binding(self): | |
| for class_name in ("GtkSourceView", "XedView"): | |
| binding_set = Gtk.binding_set_find(class_name) | |
| if binding_set: | |
| Gtk.binding_entry_remove( | |
| binding_set, Gdk.KEY_d, Gdk.ModifierType.CONTROL_MASK | |
| ) | |
| self._binding_removed = True | |
| def do_deactivate(self): | |
| """Called when the plugin is disabled or Xed closes.""" | |
| if self.window in self.handlers: | |
| for handler_id in self.handlers[self.window]: | |
| self.window.disconnect(handler_id) | |
| del self.handlers[self.window] | |
| def on_tab_added(self, window, tab): | |
| view = tab.get_view() | |
| if view: | |
| self.setup_view(view) | |
| def on_active_tab_changed(self, window, tab): | |
| if tab: | |
| view = tab.get_view() | |
| if view: | |
| self.setup_view(view) | |
| def setup_view(self, view): | |
| if not hasattr(view, '_dup_handler_set'): | |
| view.connect("key-press-event", self.on_key_press) | |
| view._dup_handler_set = True | |
| def on_key_press(self, view, event): | |
| default_mask = Gtk.accelerator_get_default_mod_mask() | |
| is_ctrl_d = ( | |
| (event.state & default_mask) == Gdk.ModifierType.CONTROL_MASK | |
| and event.keyval in (Gdk.KEY_d, Gdk.KEY_D) | |
| ) | |
| if is_ctrl_d: | |
| buf = view.get_buffer() | |
| if buf.get_has_selection(): | |
| bounds = buf.get_selection_bounds() | |
| # Handle either (start, end) or (bool, start, end) return shapes | |
| if len(bounds) == 3: | |
| _, start_iter, end_iter = bounds | |
| else: | |
| start_iter, end_iter = bounds | |
| selected_text = buf.get_text(start_iter, end_iter, True) | |
| cleaned_text = selected_text.rstrip('\r\n') | |
| if cleaned_text: | |
| buf.begin_user_action() | |
| target_offset = end_iter.get_offset() | |
| buf.select_range(end_iter, end_iter) | |
| insert_iter = buf.get_iter_at_offset(target_offset) | |
| buf.insert(insert_iter, cleaned_text) | |
| # RETURN THE CURSOR TO THE END OF THE ORIGINAL SELECTION | |
| cursor_iter = buf.get_iter_at_offset(target_offset) | |
| buf.place_cursor(cursor_iter) | |
| buf.end_user_action() | |
| return True | |
| else: | |
| buf.begin_user_action() | |
| cursor_iter = buf.get_iter_at_mark(buf.get_insert()) | |
| line_num = cursor_iter.get_line() | |
| start_line = buf.get_iter_at_line(line_num) | |
| end_line = buf.get_iter_at_line(line_num) | |
| if not end_line.ends_line(): | |
| end_line.forward_to_line_end() | |
| line_text = buf.get_text(start_line, end_line, True) | |
| buf.insert(end_line, "\n" + line_text) | |
| buf.end_user_action() | |
| return True | |
| return False |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment