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

@@ -13,54 +13,95 @@ class Commenter(CodeCommentTagsMixin):
def keyboard_tggl_comment(self, buffer):
language = buffer.get_language()
if language is None: return
language = buffer.get_language()
if not language: return
start_tag, end_tag = self.get_comment_tags(language)
# Note: Only handling line comment tag- no block comment option
if not start_tag and not end_tag: return
if not (start_tag or end_tag): return
bounds = buffer.get_selection_bounds()
if bounds:
self._bounds_comment(
start_tag, end_tag, bounds, buffer
)
else:
self._line_comment(start_tag, end_tag, buffer)
start_tag += " "
end_tag = end_tag or ""
bounds = buffer.get_selection_bounds()
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.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.end_user_action()
def _bounds_comment(self, start_tag, end_tag, bounds, buffer):
start_itr, end_itr = bounds
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 = "\n".join(
line.replace(start_tag, "") if line.startswith(start_tag) else start_tag + line
for line in text.splitlines()
(self._bounds_comment if bounds else self._line_comment)(
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:
stripped = f"{start_tag}{stripped}{end_tag}"
buffer.begin_user_action()
buffer.delete(start_itr, end_itr)
buffer.insert(start_itr, text)
buffer.delete(start, end)
buffer.insert(start, indent + stripped)
buffer.end_user_action()
buffer.place_cursor(buffer.get_iter_at_line_offset(line, col))
def _bounds_comment(self, buffer, start_tag: str, end_tag: str, bounds):
def indent_len(s): return len(s) - len(s.lstrip())
def insert(line, idx):
return f"{line[:idx]}{start_tag}{line[idx:]}{end_tag}"
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.delete(start, end)
buffer.insert(start, new_text)
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 = _close_split_view,
binding_mode = "released",
binding = "<Shift><Control>w"
binding = "<Alt>\\"
)
self.emit_to("source_views", event)

View File

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

View File

@@ -1,16 +1,22 @@
# Python imports
import os
import shlex
# Lib imports
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gio', '2.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 GLib
from gi.repository import Gdk
from gi.repository import Pango
# Application imports
from libs.event_factory import Event_Factory, Code_Event_Types
from .vte_widget import VteWidget
@@ -19,7 +25,8 @@ class TerminalsView(Gtk.Notebook):
def __init__(self):
super(TerminalsView, self).__init__()
self.code_view = None
self.MARKERS: list = ["src", ".git", ".gitignore", "README.md"]
self.code_view = None
self._setup_styling()
self._setup_signals()
@@ -58,11 +65,12 @@ class TerminalsView(Gtk.Notebook):
label = Gtk.Label(label = "...")
vte_widget = VteWidget()
vte_widget.hide_view = self.hide
vte_widget.create_terminal = self.create_terminal
vte_widget.close_terminal = self.close_terminal
vte_widget.prev_terminal = self.prev_terminal
vte_widget.next_terminal = self.next_terminal
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.close_terminal = self.close_terminal
vte_widget.prev_terminal = self.prev_terminal
vte_widget.next_terminal = self.next_terminal
label.set_text( vte_widget.get_home_path() )
label.set_tooltip_text( vte_widget.get_home_path() )
@@ -98,9 +106,65 @@ class TerminalsView(Gtk.Notebook):
def _create_terminal(self, widget):
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):
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):
label, vte_widget = self._generate_terminal_parts()
index = self.append_page(vte_widget, label)
@@ -116,7 +180,7 @@ class TerminalsView(Gtk.Notebook):
size = self.get_n_pages()
if size == 1: return
i = self.get_current_page()
i = self.get_current_page()
widget = self.get_nth_page(i)
self.remove_page(i)
widget.destroy()

View File

@@ -122,6 +122,10 @@ class VteWidget(Vte.Terminal):
ctrl_pressed = event.state & Gdk.ModifierType.CONTROL_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 shift_pressed:
if event.keyval in [Gdk.KEY_C, Gdk.KEY_V]:

View File

@@ -61,7 +61,20 @@ class SourceFile(GtkSource.File):
location: Gtk.TextIter,
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(
self,

View File

@@ -29,6 +29,7 @@ from .cursor_moved_event import CursorMovedEvent
from .delete_range_event import DeleteRangeEvent
from .modified_changed_event import ModifiedChangedEvent
from .text_changed_event import TextChangedEvent
from .text_insert_event import TextInsertEvent
from .text_inserted_event import TextInsertedEvent
from .focused_view_event import FocusedViewEvent
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