diff --git a/src/core/widgets/code/controllers/views/marker_manager.py b/src/core/widgets/code/controllers/views/marker_manager.py index 3b81b68..70bf0b1 100644 --- a/src/core/widgets/code/controllers/views/marker_manager.py +++ b/src/core/widgets/code/controllers/views/marker_manager.py @@ -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() diff --git a/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py b/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py index 94673a6..c8b5f1e 100644 --- a/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py +++ b/src/core/widgets/code/controllers/views/states/source_view_multi_insert_state.py @@ -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() ) diff --git a/src/core/widgets/code/key_mapper.py b/src/core/widgets/code/key_mapper.py index 6f788da..d561886 100644 --- a/src/core/widgets/code/key_mapper.py +++ b/src/core/widgets/code/key_mapper.py @@ -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)