Fix multi-select word movement for snake_case and add Ctrl+scroll zoom

- Implement snake_case-aware word movement that treats underscores as part of words
- Fix selection edge detection to only move when caret is at appropriate boundary
- Add INSERT state guards to line_up/down commands
- Add Ctrl+scroll zoom in/out functionality for text size
- Remove obsolete newton.zip archive
This commit is contained in:
2026-03-22 23:42:28 -05:00
parent c821f30880
commit b5cec0d049
10 changed files with 101 additions and 10 deletions

View File

@@ -4,7 +4,6 @@ ___
1. Add TreeSitter 1. Add TreeSitter
1. Add Collapsable code blocks 1. Add Collapsable code blocks
1. Add Terminal plugin 1. Add Terminal plugin
1. Add <Ctrl>mouse scroll to zoom text in/out
1. Add event to emit on file open so plugins could try to open 1. Add event to emit on file open so plugins could try to open
1. Add Plugin to <Shift\><Ctrl\>| and <Ctrl\>| to split views up, down, left, right 1. Add Plugin to <Shift\><Ctrl\>| and <Ctrl\>| to split views up, down, left, right
1. Add <Ctrl\>i to **lsp_manager** to list who implements xyz 1. Add <Ctrl\>i to **lsp_manager** to list who implements xyz
@@ -17,7 +16,5 @@ ___
___ ___
### Fix ### Fix
- Fix on lsp client unload to close files lsp side and unload server endpoint - Fix on lsp client unload to close files lsp side and unload server endpoint
- Fix multi-select <Shift\><Ctrl\> left/right block select movement de-sync
from leader when '_' in word
___ ___

View File

@@ -8,6 +8,7 @@ gi.require_version('GtkSource', '4')
from gi.repository import GtkSource from gi.repository import GtkSource
# Application imports # Application imports
from libs.dto.states import SourceViewStates
@@ -17,4 +18,6 @@ def execute(
**kwargs **kwargs
): ):
logger.debug("Command: Line Down") logger.debug("Command: Line Down")
if not view.state == SourceViewStates.INSERT: return
view.emit("move-lines", True) view.emit("move-lines", True)

View File

@@ -8,6 +8,7 @@ gi.require_version('GtkSource', '4')
from gi.repository import GtkSource from gi.repository import GtkSource
# Application imports # Application imports
from libs.dto.states import SourceViewStates
@@ -17,4 +18,6 @@ def execute(
**kwargs **kwargs
): ):
logger.debug("Command: Line Up") logger.debug("Command: Line Up")
if not view.state == SourceViewStates.INSERT: return
view.emit("move-lines", False) view.emit("move-lines", False)

View File

@@ -56,10 +56,28 @@ class MarkerManager(MarkSupportMixin):
continue continue
if has_selection: if has_selection:
caret_itr = buffer.get_iter_at_mark(end_mark)
start_itr = buffer.get_iter_at_mark(start_mark)
is_left_edge = caret_itr.compare(start_itr) <= 0
is_right_edge = not is_left_edge
can_move = (
(is_forward and is_right_edge) or
(not is_forward and is_left_edge)
)
self.collapse_selection(buffer, mark_hash, start_mark, end_mark, is_forward) self.collapse_selection(buffer, mark_hash, start_mark, end_mark, is_forward)
if mode == "word":
if not can_move: continue
itr = caret_itr
self._move_iter(buffer, itr, mode, is_forward)
buffer.move_mark(start_mark, itr)
buffer.move_mark(end_mark, itr)
continue continue
# No selection — move both anchor and caret together
# No selection - move both anchor and caret together
self._move_iter(buffer, end_itr, mode, is_forward) self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(start_mark, end_itr) buffer.move_mark(start_mark, end_itr)
@@ -81,17 +99,50 @@ class MarkerManager(MarkSupportMixin):
left = end_itr left = end_itr
right = start_itr right = start_itr
# If moving forward collapse to right edge # If moving forward -> collapse to right edge
collapse_itr = right if is_forward else left collapse_itr = right if is_forward else left
buffer.move_mark(start_mark, collapse_itr) buffer.move_mark(start_mark, collapse_itr)
buffer.move_mark(end_mark, collapse_itr) buffer.move_mark(end_mark, collapse_itr)
def move_word_snake_case(self, itr: Gtk.TextIter, count: int):
def is_word(ch):
return ch and (ch.isalnum() or ch == "_")
def step(fwd):
return itr.forward_cursor_position() if fwd else itr.backward_cursor_position()
def peek(fwd):
if fwd: return itr.get_char()
tmp = itr.copy()
return tmp.backward_cursor_position() and tmp.get_char()
def walk(fwd, cond):
while True:
ch = peek(fwd)
if not cond(ch): break
if not step(fwd): return False
return True
fwd = count > 0
for _ in range(abs(count)):
ch = itr.get_char() if fwd else peek(False)
if is_word(ch):
# inside word
if not walk(fwd, is_word): return
else:
# in separators -> skip them, then the word
if not walk(fwd, lambda c: not is_word(c)): return
if not walk(fwd, is_word): return
def _move_iter(self, buffer, itr_, mode: str, is_forward: bool): def _move_iter(self, buffer, itr_, mode: str, is_forward: bool):
if mode == "char": if mode == "char":
itr_.forward_char() if is_forward else itr_.backward_char() itr_.forward_char() if is_forward else itr_.backward_char()
elif mode == "word": elif mode == "word":
itr_.forward_word_end() if is_forward else itr_.backward_word_start() self.move_word_snake_case(itr_, 1 if is_forward else -1)
elif mode == "line": elif mode == "line":
line = itr_.get_line() line = itr_.get_line()
offset = itr_.get_line_offset() offset = itr_.get_line_offset()

View File

@@ -57,6 +57,7 @@ class SourceViewSignalMapper:
"key-release-event": self._key_release_event, "key-release-event": self._key_release_event,
"button-press-event": self._button_press_event, "button-press-event": self._button_press_event,
"button-release-event": self._button_release_event, "button-release-event": self._button_release_event,
"scroll-event": self._scroll_event,
"populate-popup": self._populate_popup "populate-popup": self._populate_popup
} }
@@ -81,5 +82,8 @@ class SourceViewSignalMapper:
def _button_release_event(self, source_view: SourceView, eve): def _button_release_event(self, source_view: SourceView, eve):
return self.state_manager.handle_button_release_event(source_view, eve) return self.state_manager.handle_button_release_event(source_view, eve)
def _populate_popup(self, source_view, menu): def _scroll_event(self, source_view: SourceView, eve):
return self.state_manager.handle_scroll_event(source_view, eve)
def _populate_popup(self, source_view: SourceView, menu):
return self.state_manager.handle_populate_popup(source_view, menu, self.emit) return self.state_manager.handle_populate_popup(source_view, menu, self.emit)

View File

@@ -53,8 +53,15 @@ class SourceViewStateManager:
def handle_button_release_event(self, source_view, eve): def handle_button_release_event(self, source_view, eve):
return self.states[source_view.state].button_release_event(source_view, eve) return self.states[source_view.state].button_release_event(source_view, eve)
def handle_scroll_event(self, source_view, eve):
return self.states[source_view.state].scroll_event(
source_view, eve, self.key_mapper
)
def handle_populate_popup(self, source_view, menu, emit): def handle_populate_popup(self, source_view, menu, emit):
return self.states[source_view.state].populate_popup(source_view, menu, emit) return self.states[source_view.state].populate_popup(
source_view, menu, emit
)
def _handle_multi_insert_toggle(self, source_view, eve): def _handle_multi_insert_toggle(self, source_view, eve):
is_control = self.key_mapper.is_control(eve) is_control = self.key_mapper.is_control(eve)

View File

@@ -1,6 +1,9 @@
# Python imports # Python imports
# Lib imports # Lib imports
import gi
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
# Application imports # Application imports
from libs.event_factory import Event_Factory, Code_Event_Types from libs.event_factory import Event_Factory, Code_Event_Types
@@ -79,6 +82,29 @@ class SourceViewsBaseState:
return True if not response else response return True if not response else response
def scroll_event(self, source_view, eve, key_mapper):
is_control = key_mapper.is_control(eve)
if not is_control: return
if eve.direction == Gdk.ScrollDirection.SMOOTH:
has_deltas, dx, dy = eve.get_scroll_deltas()
if not has_deltas: return False
if dy < 0:
source_view.command.exec("zoom_in")
elif dy > 0:
source_view.command.exec("zoom_out")
return True
if eve.direction == Gdk.ScrollDirection.UP:
source_view.command.exec("zoom_in")
elif eve.direction == Gdk.ScrollDirection.DOWN:
source_view.command.exec("zoom_out")
return True
def populate_popup(self, source_view, menu, emit): def populate_popup(self, source_view, menu, emit):
buffer = source_view.get_buffer() buffer = source_view.get_buffer()
event = Event_Factory.create_event( event = Event_Factory.create_event(

View File

@@ -61,7 +61,6 @@ class SourceViewsMultiInsertState(SourceViewsBaseState):
self.marker_manager.apply_to_marks(buffer, replace_word) self.marker_manager.apply_to_marks(buffer, replace_word)
return True return True
def move_cursor(self, source_view, step, count, is_selection, emit): def move_cursor(self, source_view, step, count, is_selection, emit):
is_forward = count > 0 is_forward = count > 0
buffer = source_view.get_buffer() buffer = source_view.get_buffer()
@@ -78,6 +77,8 @@ class SourceViewsMultiInsertState(SourceViewsBaseState):
self._signal_cursor_moved(source_view, emit) self._signal_cursor_moved(source_view, emit)
return False
def key_press_event(self, source_view, event, key_mapper): def key_press_event(self, source_view, event, key_mapper):
char = key_mapper.get_raw_keyname(event).upper() char = key_mapper.get_raw_keyname(event).upper()
self.is_control = key_mapper.is_control(event) self.is_control = key_mapper.is_control(event)

View File

@@ -106,7 +106,6 @@ class MarkSupportMixin:
name = f"multi-insert-end-{hash}", name = f"multi-insert-end-{hash}",
left_gravity = False left_gravity = False
) )
# left_gravity = True
buffer.add_mark(start_mark, target_iter) buffer.add_mark(start_mark, target_iter)
buffer.add_mark(end_mark, target_iter) buffer.add_mark(end_mark, target_iter)

Binary file not shown.