Skip to content

Instantly share code, notes, and snippets.

@acidnine
Last active August 6, 2026 05:51
Show Gist options
  • Select an option

  • Save acidnine/241516d5fb7bf8feb4dd9581ec190fb4 to your computer and use it in GitHub Desktop.

Select an option

Save acidnine/241516d5fb7bf8feb4dd9581ec190fb4 to your computer and use it in GitHub Desktop.
XED Plugin Selection Duplicate
[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/
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