Created
July 31, 2026 16:38
-
-
Save acidnine/682a3edaba584d0eca682d0f4d60abcf to your computer and use it in GitHub Desktop.
XED Plugin Toggle Comment
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=toggle_comment | |
| IAge=3 | |
| Name=Toggle Comment | |
| Description=Toggle a line comment prefix (# by default) on the current line or all selected lines (Ctrl+Shift+C). | |
| Authors=Acidnine; Gemini / ChatGPT / Claude | |
| Copyright=Copyright © 2026 Acidnine | |
| Website=https://vault21.net/ | |
| Version=1.0.0 |
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('Gtk', '3.0') | |
| gi.require_version('Xed', '1.0') | |
| from gi.repository import GObject, Gio, Gtk, Xed | |
| # Change this if you want a different comment marker by default. | |
| COMMENT_PREFIX = "#" | |
| class ToggleCommentPlugin(GObject.Object, Xed.WindowActivatable): | |
| __gtype_name__ = "ToggleCommentPlugin" | |
| window = GObject.Property(type=Xed.Window) | |
| def __init__(self): | |
| GObject.Object.__init__(self) | |
| self._action = None | |
| self._accel_set = False | |
| self._connected_views = {} | |
| self._tab_added_id = None | |
| def do_activate(self): | |
| #print("[toggle-comment] do_activate called") | |
| self._action = Gio.SimpleAction.new("toggle-comment", None) | |
| self._action.connect("activate", self.on_toggle_comment) | |
| self.window.add_action(self._action) | |
| self._try_set_accel() | |
| for view in self.window.get_views(): | |
| self._connect_view(view) | |
| self._tab_added_id = self.window.connect("tab-added", self.on_tab_added) | |
| def _try_set_accel(self): | |
| if self._accel_set: | |
| return | |
| app = self.window.get_application() | |
| if app is None: | |
| #print("[toggle-comment] window has no application yet, will retry") | |
| return | |
| app.set_accels_for_action("win.toggle-comment", ["<Primary><Shift>c"]) | |
| self._accel_set = True | |
| #print("[toggle-comment] accelerator registered") | |
| def do_deactivate(self): | |
| if self._tab_added_id is not None: | |
| self.window.disconnect(self._tab_added_id) | |
| self._tab_added_id = None | |
| for view, handler_id in self._connected_views.items(): | |
| view.disconnect(handler_id) | |
| self._connected_views.clear() | |
| self.window.remove_action("toggle-comment") | |
| def do_update_state(self): | |
| self._try_set_accel() | |
| view = self.window.get_active_view() | |
| if self._action is not None: | |
| self._action.set_enabled(view is not None and view.get_editable()) | |
| def on_toggle_comment(self, action, parameter): | |
| #print("[toggle-comment] action activated") | |
| view = self.window.get_active_view() | |
| if view is None or not view.get_editable(): | |
| return | |
| doc = view.get_buffer() | |
| self.toggle_comment(doc) | |
| def on_tab_added(self, window, tab): | |
| view = tab.get_view() | |
| self._connect_view(view) | |
| def _connect_view(self, view): | |
| if view in self._connected_views: | |
| return | |
| handler_id = view.connect("populate-popup", self.on_populate_popup) | |
| self._connected_views[view] = handler_id | |
| def on_populate_popup(self, view, popup): | |
| if isinstance(popup, Gtk.Menu): | |
| separator = Gtk.SeparatorMenuItem() | |
| separator.show() | |
| popup.append(separator) | |
| item = Gtk.MenuItem(label="Toggle Comment") | |
| item.connect("activate", lambda w: self._action.activate(None)) | |
| item.show() | |
| popup.append(item) | |
| elif isinstance(popup, Gio.Menu): | |
| section = Gio.Menu() | |
| section.append("Toggle Comment", "win.toggle-comment") | |
| popup.append_section(None, section) | |
| def toggle_comment(self, doc): | |
| if doc.get_has_selection(): | |
| start, end = doc.get_selection_bounds() | |
| else: | |
| it = doc.get_iter_at_mark(doc.get_insert()) | |
| start = end = it | |
| start_line = start.get_line() | |
| end_line = end.get_line() | |
| # If the selection ends right at the start of a line (e.g. you | |
| # selected down to col 0 of the next line), don't treat that | |
| # trailing line as part of the selection. | |
| if end_line > start_line and end.starts_line(): | |
| end_line -= 1 | |
| line_numbers = list(range(start_line, end_line + 1)) | |
| should_uncomment = self._all_lines_commented(doc, line_numbers) | |
| doc.begin_user_action() | |
| try: | |
| for ln in line_numbers: | |
| if should_uncomment: | |
| self._uncomment_line(doc, ln) | |
| else: | |
| self._comment_line(doc, ln) | |
| finally: | |
| doc.end_user_action() | |
| def _all_lines_commented(self, doc, line_numbers): | |
| saw_content = False | |
| for ln in line_numbers: | |
| text = self._line_text(doc, ln) | |
| stripped = text.lstrip() | |
| if stripped == "": | |
| continue | |
| saw_content = True | |
| if not stripped.startswith(COMMENT_PREFIX): | |
| return False | |
| # If the selection was entirely blank lines, treat as "comment" action. | |
| return saw_content | |
| def _line_text(self, doc, ln): | |
| start_it = doc.get_iter_at_line(ln) | |
| end_it = start_it.copy() | |
| if not end_it.ends_line(): | |
| end_it.forward_to_line_end() | |
| return doc.get_text(start_it, end_it, False) | |
| def _comment_line(self, doc, ln): | |
| text = self._line_text(doc, ln) | |
| if text.strip() == "": | |
| return # leave blank lines alone | |
| indent_len = len(text) - len(text.lstrip()) | |
| insert_it = doc.get_iter_at_line_offset(ln, indent_len) | |
| doc.insert(insert_it, COMMENT_PREFIX + " ") | |
| def _uncomment_line(self, doc, ln): | |
| text = self._line_text(doc, ln) | |
| indent_len = len(text) - len(text.lstrip()) | |
| after_indent = text[indent_len:] | |
| if after_indent.startswith(COMMENT_PREFIX + " "): | |
| remove_len = len(COMMENT_PREFIX) + 1 | |
| elif after_indent.startswith(COMMENT_PREFIX): | |
| remove_len = len(COMMENT_PREFIX) | |
| else: | |
| return | |
| start_it = doc.get_iter_at_line_offset(ln, indent_len) | |
| end_it = doc.get_iter_at_line_offset(ln, indent_len + remove_len) | |
| doc.delete(start_it, end_it) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment