feat(code): improve comment toggling, terminal navigation, and editor event wiring

- Refactor Commenter toggle logic for line and multi-line comments
  - Preserve indentation and cursor position
  - Improve handling of existing comment detection and removal
  - Simplify bounds vs line comment dispatch

- Enhance terminal project navigation
  - Add project marker detection via Gio file traversal
  - Implement go-to-project-or-home behavior (Home key shortcut)
  - Automatically `cd` into detected project root or home directory
  - Wire terminal widget navigation through VteWidget

- Improve terminal integration
  - Pass emit_to into terminals view for event dispatching
  - Add ability for VteWidget to trigger project navigation

- Update split pane shortcut
  - Change close split view binding to Alt+\

- Add editor event support
  - Emit `text_insert` event from SourceFile on insert
  - Add new TextInsertEvent DTO and register in event system

- Misc cleanup
  - Improve imports and structure in terminals module
  - Add project marker list and filesystem traversal helpers
This commit is contained in:
2026-04-15 01:54:56 -05:00
parent 12b5fe7304
commit 41f3501e1f
8 changed files with 198 additions and 54 deletions

View File

@@ -14,53 +14,94 @@ class Commenter(CodeCommentTagsMixin):
def keyboard_tggl_comment(self, buffer): def keyboard_tggl_comment(self, buffer):
language = buffer.get_language() language = buffer.get_language()
if language is None: return if not language: return
start_tag, end_tag = self.get_comment_tags(language) start_tag, end_tag = self.get_comment_tags(language)
# Note: Only handling line comment tag- no block comment option if not (start_tag or end_tag): return
if not start_tag and not end_tag: return
start_tag += " "
end_tag = end_tag or ""
bounds = buffer.get_selection_bounds() bounds = buffer.get_selection_bounds()
if bounds:
self._bounds_comment( (self._bounds_comment if bounds else self._line_comment)(
start_tag, end_tag, bounds, buffer buffer, start_tag, end_tag, bounds
) )
def _line_comment(self, buffer, start_tag: str, end_tag: str, bounds):
start = buffer.get_iter_at_mark(buffer.get_insert()).copy()
end = start.copy()
line, col = start.get_line() + 1, start.get_line_offset()
if not start.starts_line():
start.set_line_offset(0)
if not end.ends_line():
end.forward_to_line_end()
text = buffer.get_text(start, end, True)
stripped = text.lstrip()
indent = text[:-len(stripped)] if stripped else text
if stripped.startswith(start_tag):
stripped = stripped[len(start_tag):].lstrip().replace(end_tag, "", 1)
else: else:
self._line_comment(start_tag, end_tag, buffer) stripped = f"{start_tag}{stripped}{end_tag}"
def _line_comment(self, start_tag, end_tag, buffer):
start_itr = buffer.get_iter_at_mark( buffer.get_insert() ).copy()
end_itr = start_itr.copy()
if not start_itr.starts_line():
start_itr.set_line_offset(0)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
text = buffer.get_text(start_itr, end_itr, True)
text = text.replace(start_tag, "") if text.startswith(start_tag) else start_tag + text
buffer.begin_user_action() buffer.begin_user_action()
buffer.delete(start_itr, end_itr) buffer.delete(start, end)
buffer.insert(start_itr, text) buffer.insert(start, indent + stripped)
buffer.end_user_action() buffer.end_user_action()
buffer.place_cursor(buffer.get_iter_at_line_offset(line, col))
def _bounds_comment(self, start_tag, end_tag, bounds, buffer): def _bounds_comment(self, buffer, start_tag: str, end_tag: str, bounds):
start_itr, end_itr = bounds def indent_len(s): return len(s) - len(s.lstrip())
if not start_itr.starts_line():
start_itr.set_line_offset(0)
if not end_itr.ends_line():
end_itr.forward_to_line_end()
text = buffer.get_text(start_itr, end_itr, True) def insert(line, idx):
text = "\n".join( return f"{line[:idx]}{start_tag}{line[idx:]}{end_tag}"
line.replace(start_tag, "") if line.startswith(start_tag) else start_tag + line
for line in text.splitlines() def process(lines):
base_indent = min(
(indent_len(l) for l in lines if l.strip()),
default = 0
) )
is_commented = all(
l.lstrip().startswith(start_tag)
for l in lines if l.strip()
)
if is_commented:
return [
l.replace(start_tag, "", 1).replace(end_tag, "", 1)
if l.lstrip().startswith(start_tag.lstrip())
else l
for l in lines
]
return [
l if not l.strip()
else insert(l, base_indent)
for l in lines
]
start, end = bounds
sline, scol = start.get_line(), start.get_line_offset()
eline, ecol = end.get_line(), end.get_line_offset()
if not start.starts_line():
start.set_line_offset(0)
if not end.ends_line():
end.forward_to_line_end()
lines = buffer.get_text(start, end, True).splitlines()
new_text = "\n".join(process(lines))
buffer.begin_user_action() buffer.begin_user_action()
buffer.delete(start_itr, end_itr) buffer.delete(start, end)
buffer.insert(start_itr, text) buffer.insert(start, new_text)
buffer.end_user_action() buffer.end_user_action()
buffer.select_range(
buffer.get_iter_at_line_offset(sline, scol),
buffer.get_iter_at_line_offset(eline, ecol),
)

View File

@@ -55,7 +55,7 @@ class Plugin(PluginCode):
command_name = "close_split_view", command_name = "close_split_view",
command = _close_split_view, command = _close_split_view,
binding_mode = "released", binding_mode = "released",
binding = "<Shift><Control>w" binding = "<Alt>\\"
) )
self.emit_to("source_views", event) self.emit_to("source_views", event)

View File

@@ -24,6 +24,7 @@ class Plugin(PluginCode):
... ...
def load(self): def load(self):
terminals_view.emit_to = self.emit_to
footer = self.request_ui_element("footer-container") footer = self.request_ui_element("footer-container")
footer.add( terminals_view ) footer.add( terminals_view )

View File

@@ -1,16 +1,22 @@
# Python imports # Python imports
import os
import shlex
# Lib imports # Lib imports
import gi import gi
gi.require_version('Gtk', '3.0') gi.require_version('Gio', '2.0')
gi.require_version('Gdk', '3.0') gi.require_version('Gdk', '3.0')
gi.require_version('Gtk', '3.0')
from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import Pango from gi.repository import Pango
# Application imports # Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from .vte_widget import VteWidget from .vte_widget import VteWidget
@@ -19,6 +25,7 @@ class TerminalsView(Gtk.Notebook):
def __init__(self): def __init__(self):
super(TerminalsView, self).__init__() super(TerminalsView, self).__init__()
self.MARKERS: list = ["src", ".git", ".gitignore", "README.md"]
self.code_view = None self.code_view = None
self._setup_styling() self._setup_styling()
@@ -59,6 +66,7 @@ class TerminalsView(Gtk.Notebook):
vte_widget = VteWidget() vte_widget = VteWidget()
vte_widget.hide_view = self.hide vte_widget.hide_view = self.hide
vte_widget.go_to_project_or_home = self.go_to_project_or_home
vte_widget.create_terminal = self.create_terminal vte_widget.create_terminal = self.create_terminal
vte_widget.close_terminal = self.close_terminal vte_widget.close_terminal = self.close_terminal
vte_widget.prev_terminal = self.prev_terminal vte_widget.prev_terminal = self.prev_terminal
@@ -98,9 +106,65 @@ class TerminalsView(Gtk.Notebook):
def _create_terminal(self, widget): def _create_terminal(self, widget):
self.create_terminal() self.create_terminal()
def has_marker(self, gfile):
try:
enumerator = gfile.enumerate_children(
"standard::name,standard::type",
Gio.FileQueryInfoFlags.NONE,
None
)
while True:
info = enumerator.next_file(None)
if info is None:
break
if info.get_name() in self.MARKERS:
enumerator.close(None)
return True
enumerator.close(None)
except Exception:
pass
return False
def find_project_path_or_home(self, current: Gio.File):
if not current: return
home = Gio.File.new_for_path( os.path.expanduser("~") )
while True:
if self.has_marker(current):
return current.get_path()
if current.equal(home):
return current.get_path()
parent = current.get_parent()
if parent is None:
return current.get_path()
current = parent
def set_code_view(self, widget): def set_code_view(self, widget):
self.code_view = widget self.code_view = widget
def go_to_project_or_home(self):
event = Event_Factory.create_event("get_file",
buffer = self.code_view.get_buffer()
)
self.emit_to("files", event)
if event.response.ftype == "buffer": return
gfile = event.response.get_location().get_parent()
fpath = self.find_project_path_or_home(gfile)
i = self.get_current_page()
widget = self.get_nth_page(i)
widget.run_command(f"cd {shlex.quote(fpath)} && clear\n")
def create_terminal(self): def create_terminal(self):
label, vte_widget = self._generate_terminal_parts() label, vte_widget = self._generate_terminal_parts()
index = self.append_page(vte_widget, label) index = self.append_page(vte_widget, label)

View File

@@ -122,6 +122,10 @@ class VteWidget(Vte.Terminal):
ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_MASK
shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK shift_pressed = event.state & Gdk.ModifierType.SHIFT_MASK
if event.keyval == Gdk.KEY_Home:
self.go_to_project_or_home()
return True
if ctrl_pressed: if ctrl_pressed:
if shift_pressed: if shift_pressed:
if event.keyval in [Gdk.KEY_C, Gdk.KEY_V]: if event.keyval in [Gdk.KEY_C, Gdk.KEY_V]:

View File

@@ -61,7 +61,20 @@ class SourceFile(GtkSource.File):
location: Gtk.TextIter, location: Gtk.TextIter,
text: str, length: int text: str, length: int
): ):
... event = Event_Factory.create_event(
"text_insert",
file = self,
buffer = self.buffer,
location = location,
text = text,
length = length
)
# Note: 'idle_add' needed b/c markers don't get thir positions
# updated relative to the initial insert.
# If not used, seg faults galor during multi insert.
# GLib.idle_add(self.emit, event)
self.emit(event)
def _after_insert_text( def _after_insert_text(
self, self,

View File

@@ -29,6 +29,7 @@ from .cursor_moved_event import CursorMovedEvent
from .delete_range_event import DeleteRangeEvent from .delete_range_event import DeleteRangeEvent
from .modified_changed_event import ModifiedChangedEvent from .modified_changed_event import ModifiedChangedEvent
from .text_changed_event import TextChangedEvent from .text_changed_event import TextChangedEvent
from .text_insert_event import TextInsertEvent
from .text_inserted_event import TextInsertedEvent from .text_inserted_event import TextInsertedEvent
from .focused_view_event import FocusedViewEvent from .focused_view_event import FocusedViewEvent
from .set_active_file_event import SetActiveFileEvent from .set_active_file_event import SetActiveFileEvent

View File

@@ -0,0 +1,20 @@
# Python imports
from dataclasses import dataclass, field
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
# Application imports
from .code_event import CodeEvent
@dataclass
class TextInsertEvent(CodeEvent):
location: Gtk.TextIter = None
text: str = ""
length: int = 0