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:
@@ -46,14 +46,24 @@ class MarkerManager(MarkSupportMixin):
|
||||
start_itr = buffer.get_iter_at_mark(start_mark)
|
||||
end_itr = buffer.get_iter_at_mark(end_mark)
|
||||
|
||||
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
|
||||
return
|
||||
|
||||
if has_selection:
|
||||
caret_itr = buffer.get_iter_at_mark(end_mark)
|
||||
@@ -67,15 +77,14 @@ class MarkerManager(MarkSupportMixin):
|
||||
|
||||
self.collapse_selection(buffer, mark_hash, start_mark, end_mark, is_forward)
|
||||
if mode == "word":
|
||||
if not can_move: continue
|
||||
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)
|
||||
|
||||
continue
|
||||
|
||||
return
|
||||
|
||||
# No selection - move both anchor and caret together
|
||||
self._move_iter(buffer, end_itr, mode, is_forward)
|
||||
@@ -86,6 +95,7 @@ class MarkerManager(MarkSupportMixin):
|
||||
def collapse_selection(self,
|
||||
buffer, mark_hash, start_mark, end_mark, is_forward: bool
|
||||
):
|
||||
if mark_hash:
|
||||
self.buffer_markers[mark_hash]["is_selection"] = False
|
||||
|
||||
start_itr = buffer.get_iter_at_mark(start_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()
|
||||
|
||||
@@ -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):
|
||||
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() )
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user