|
10 | 10 | import weakref |
11 | 11 | from typing import Callable, Dict, Iterable, List, NoReturn, Pattern, cast |
12 | 12 |
|
| 13 | +import wcwidth |
| 14 | + |
13 | 15 | from .clipboard import ClipboardData |
14 | 16 | from .filters import vi_mode |
15 | 17 | from .selection import PasteMode, SelectionState, SelectionType |
@@ -158,13 +160,22 @@ def selection(self) -> SelectionState | None: |
158 | 160 |
|
159 | 161 | @property |
160 | 162 | def current_char(self) -> str: |
161 | | - """Return character under cursor or an empty string.""" |
162 | | - return self._get_char_relative_to_cursor(0) or "" |
| 163 | + """Return grapheme cluster under cursor or an empty string.""" |
| 164 | + text_after = self.text_after_cursor |
| 165 | + if not text_after: |
| 166 | + return "" |
| 167 | + for grapheme in wcwidth.iter_graphemes(text_after): |
| 168 | + return grapheme |
| 169 | + return "" |
163 | 170 |
|
164 | 171 | @property |
165 | 172 | def char_before_cursor(self) -> str: |
166 | | - """Return character before the cursor or an empty string.""" |
167 | | - return self._get_char_relative_to_cursor(-1) or "" |
| 173 | + """Return grapheme cluster before the cursor or an empty string.""" |
| 174 | + text_before = self.text_before_cursor |
| 175 | + if not text_before: |
| 176 | + return "" |
| 177 | + boundary = wcwidth.grapheme_boundary_before(text_before, len(text_before)) |
| 178 | + return text_before[boundary:] |
168 | 179 |
|
169 | 180 | @property |
170 | 181 | def text_before_cursor(self) -> str: |
@@ -251,15 +262,6 @@ def leading_whitespace_in_current_line(self) -> str: |
251 | 262 | length = len(current_line) - len(current_line.lstrip()) |
252 | 263 | return current_line[:length] |
253 | 264 |
|
254 | | - def _get_char_relative_to_cursor(self, offset: int = 0) -> str: |
255 | | - """ |
256 | | - Return character relative to cursor position, or empty string |
257 | | - """ |
258 | | - try: |
259 | | - return self.text[self.cursor_position + offset] |
260 | | - except IndexError: |
261 | | - return "" |
262 | | - |
263 | 265 | @property |
264 | 266 | def on_first_line(self) -> bool: |
265 | 267 | """ |
@@ -692,21 +694,44 @@ def find_previous_matching_line( |
692 | 694 |
|
693 | 695 | def get_cursor_left_position(self, count: int = 1) -> int: |
694 | 696 | """ |
695 | | - Relative position for cursor left. |
| 697 | + Relative position for cursor left (grapheme cluster aware). |
696 | 698 | """ |
697 | 699 | if count < 0: |
698 | 700 | return self.get_cursor_right_position(-count) |
699 | 701 |
|
700 | | - return -min(self.cursor_position_col, count) |
| 702 | + line_before = self.current_line_before_cursor |
| 703 | + if not line_before: |
| 704 | + return 0 |
| 705 | + |
| 706 | + pos = len(line_before) |
| 707 | + for _ in range(count): |
| 708 | + if pos <= 0: |
| 709 | + break |
| 710 | + new_pos = wcwidth.grapheme_boundary_before(line_before, pos) |
| 711 | + if new_pos == pos: |
| 712 | + break |
| 713 | + pos = new_pos |
| 714 | + |
| 715 | + return pos - len(line_before) |
701 | 716 |
|
702 | 717 | def get_cursor_right_position(self, count: int = 1) -> int: |
703 | 718 | """ |
704 | | - Relative position for cursor_right. |
| 719 | + Relative position for cursor right (grapheme cluster aware). |
705 | 720 | """ |
706 | 721 | if count < 0: |
707 | 722 | return self.get_cursor_left_position(-count) |
708 | 723 |
|
709 | | - return min(count, len(self.current_line_after_cursor)) |
| 724 | + line_after = self.current_line_after_cursor |
| 725 | + if not line_after: |
| 726 | + return 0 |
| 727 | + |
| 728 | + pos = 0 |
| 729 | + for i, grapheme in enumerate(wcwidth.iter_graphemes(line_after)): |
| 730 | + if i >= count: |
| 731 | + break |
| 732 | + pos += len(grapheme) |
| 733 | + |
| 734 | + return pos |
710 | 735 |
|
711 | 736 | def get_cursor_up_position( |
712 | 737 | self, count: int = 1, preferred_column: int | None = None |
|
0 commit comments