refactor(cursor): centralize movement logic and enhance word navigation

* Extract `_proc_move` to unify cursor/selection handling across markers
* Rework multi-insert cursor flow with `_do_cursor_moved` and improved key routing
* Add `ignore_leader` support for independent leader cursor movement
* Replace `move_word_snake_case` with `move_along_word` (better punctuation/special char handling)
* Add `is_super` modifier support in KeyMapper
This commit is contained in:
2026-03-30 00:45:08 -05:00
parent bd277c0214
commit e367e31890
3 changed files with 161 additions and 61 deletions

View File

@@ -46,47 +46,57 @@ class MarkerManager(MarkSupportMixin):
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
if is_selection:
self._proc_move(
buffer, is_forward, is_selection, mode, mark_hash,
start_mark, end_mark, has_selection, start_itr, end_itr
)
def _proc_move(
self, buffer, is_forward: bool, is_selection: bool, mode: str,
mark_hash, start_mark, end_mark, has_selection, start_itr, end_itr
):
if is_selection:
if mark_hash:
self.buffer_markers[mark_hash]["is_selection"] = True
self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(end_mark, end_itr)
self._apply_selection(buffer, start_itr, end_itr)
continue
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)
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
# No selection - move both anchor and caret together
self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(start_mark, end_itr)
buffer.move_mark(end_mark, end_itr)
self._apply_selection(buffer, start_itr, end_itr)
return
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)
if mode == "word":
if not can_move: return
itr = caret_itr
self._move_iter(buffer, itr, mode, is_forward)
buffer.move_mark(start_mark, itr)
buffer.move_mark(end_mark, itr)
return
# No selection - move both anchor and caret together
self._move_iter(buffer, end_itr, mode, is_forward)
buffer.move_mark(start_mark, end_itr)
buffer.move_mark(end_mark, end_itr)
def collapse_selection(self,
buffer, mark_hash, start_mark, end_mark, is_forward: bool
):
self.buffer_markers[mark_hash]["is_selection"] = False
if mark_hash:
self.buffer_markers[mark_hash]["is_selection"] = False
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
@@ -105,19 +115,28 @@ class MarkerManager(MarkSupportMixin):
buffer.move_mark(start_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):
def move_along_word(self, itr: Gtk.TextIter, count: int):
def not_is_word(ch: str):
return not is_word(ch)
def is_word(ch: str):
return ch and (ch.isalnum() or ch == "_")
def step(fwd):
def is_special(ch: str):
return ch in "-"
def is_punct(ch: str):
return ch in ".;?!"
def step(fwd: bool):
return itr.forward_cursor_position() if fwd else itr.backward_cursor_position()
def peek(fwd):
def peek(fwd: bool):
if fwd: return itr.get_char()
tmp = itr.copy()
return tmp.backward_cursor_position() and tmp.get_char()
def walk(fwd, cond):
def walk(fwd, cond: callable):
while True:
ch = peek(fwd)
if not cond(ch): break
@@ -126,23 +145,22 @@ class MarkerManager(MarkSupportMixin):
return True
fwd = count > 0
for _ in range( abs(count) ):
ch = peek(fwd)
for _ in range(abs(count)):
ch = itr.get_char() if fwd else peek(False)
if is_word(ch):
# inside word
if is_special(ch) or is_punct(ch):
step(fwd)
elif is_word(ch):
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, not_is_word): return
if not walk(fwd, is_word): return
def _move_iter(self, buffer, itr_, mode: str, is_forward: bool):
if mode == "char":
itr_.forward_char() if is_forward else itr_.backward_char()
elif mode == "word":
self.move_word_snake_case(itr_, 1 if is_forward else -1)
self.move_along_word(itr_, 1 if is_forward else -1)
elif mode == "line":
line = itr_.get_line()
offset = itr_.get_line_offset()

View File

@@ -61,28 +61,53 @@ class SourceViewsMultiInsertState(SourceViewsBaseState):
self.marker_manager.apply_to_marks(buffer, replace_word)
return True
def move_cursor(self, source_view, step, count, is_selection, emit):
is_forward = count > 0
buffer = source_view.get_buffer()
def move_cursor(
self, source_view, step, count, is_selection,
emit = None, ignore_leader: bool = False
):
is_forward = count > 0
buffer = source_view.get_buffer()
if step in [
Gtk.MovementStep.LOGICAL_POSITIONS,
Gtk.MovementStep.VISUAL_POSITIONS
]:
self.marker_manager.move_by_char(buffer, is_forward, is_selection)
elif step == Gtk.MovementStep.WORDS:
self.marker_manager.move_by_word(buffer, is_forward, is_selection)
elif step == Gtk.MovementStep.DISPLAY_LINES:
self.marker_manager.move_by_line(buffer, is_forward, is_selection)
start_mark = buffer.get_insert()
end_mark = buffer.get_selection_bound()
start_itr = buffer.get_iter_at_mark(start_mark)
end_itr = buffer.get_iter_at_mark(end_mark)
has_selection = not start_itr.equal(end_itr)
self._signal_cursor_moved(source_view, emit)
step_map = {
Gtk.MovementStep.LOGICAL_POSITIONS: ("char", self.marker_manager.move_by_char),
Gtk.MovementStep.VISUAL_POSITIONS: ("char", self.marker_manager.move_by_char),
Gtk.MovementStep.WORDS: ("word", self.marker_manager.move_by_word),
Gtk.MovementStep.DISPLAY_LINES: ("line", self.marker_manager.move_by_line),
}
return False
kind, move_fn = step_map[step]
move_fn(buffer, is_forward, is_selection)
if ignore_leader: return True
self.marker_manager._proc_move(
buffer,
is_forward,
is_selection,
kind,
None,
start_mark,
end_mark,
has_selection,
start_itr,
end_itr,
)
# self._signal_cursor_moved(source_view, emit)
return True
def key_press_event(self, source_view, event, key_mapper):
char = key_mapper.get_raw_keyname(event).upper()
self.is_control = key_mapper.is_control(event)
self.is_shift = key_mapper.is_shift(event)
self.is_super = key_mapper.is_super(event)
if char.upper() in ["BACKSPACE", "DELETE", "ENTER"]:
self.marker_manager.process_cursor_action(
@@ -91,6 +116,9 @@ class SourceViewsMultiInsertState(SourceViewsBaseState):
)
return False
if self._do_cursor_moved(source_view, char):
return True
return super().key_press_event(source_view, event, key_mapper)
def button_press_event(self, source_view, event):
@@ -99,6 +127,56 @@ class SourceViewsMultiInsertState(SourceViewsBaseState):
def button_release_event(self, source_view, event):
self.marker_manager.button_release_event(source_view, event)
def _do_cursor_moved(self, source_view, char: str):
key = char.upper()
if key not in {"LEFT", "RIGHT", "UP", "DOWN"}: return False
direction = {
"LEFT": -1,
"RIGHT": 1,
"UP": -1,
"DOWN": 1,
}[key]
is_horizontal = key in {"LEFT", "RIGHT"}
step = \
Gtk.MovementStep.VISUAL_POSITIONS if is_horizontal else Gtk.MovementStep.DISPLAY_LINES
count = direction
is_selection = self.is_shift
if is_horizontal:
if self.is_control:
step = Gtk.MovementStep.WORDS
if self.is_control and self.is_shift:
is_selection = True
if self.is_super:
return self.move_cursor(
source_view,
step,
count,
is_selection = False,
emit = None,
ignore_leader = True,
)
else:
if self.is_super:
return self.move_cursor(
source_view,
step,
count,
is_selection,
emit = None,
ignore_leader = True,
)
return self.move_cursor(
source_view,
step = step,
count = count,
is_selection = is_selection,
)
def _signal_cursor_moved(self, source_view, emit):
buffer = source_view.get_buffer()
itr = buffer.get_iter_at_mark( buffer.get_insert() )

View File

@@ -145,7 +145,7 @@ class KeyMapper:
is_shift, \
is_alt = self.get_modkeys_states(eve)
self.state = NoKeyState
self.state = NoKeyState
if is_control:
self.state = self.state | CtrlKeyState
if is_shift:
@@ -161,6 +161,10 @@ class KeyMapper:
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
return modifiers & Gdk.ModifierType.SHIFT_MASK
def is_super(self, eve):
modifiers = Gdk.ModifierType(eve.get_state() & ~Gdk.ModifierType.LOCK_MASK)
return modifiers & Gdk.ModifierType.SUPER_MASK
def get_raw_keyname(self, eve) -> str:
return Gdk.keyval_name(eve.keyval)