Skip to content

Instantly share code, notes, and snippets.

@acidnine
Created July 31, 2026 16:38
Show Gist options
  • Select an option

  • Save acidnine/682a3edaba584d0eca682d0f4d60abcf to your computer and use it in GitHub Desktop.

Select an option

Save acidnine/682a3edaba584d0eca682d0f4d60abcf to your computer and use it in GitHub Desktop.
XED Plugin Toggle Comment
[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
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