Fix multi-select word movement for snake_case and add Ctrl+scroll zoom
- Implement snake_case-aware word movement treating underscores as part of words - Fix selection edge detection to only move word markers when caret is at appropriate boundary - Add INSERT state guards to line_up/down commands to prevent movement in non-insert modes - Add Ctrl+scroll zoom in/out functionality for text size
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user