Source code for zelfred.line_editor

# -*- coding: utf-8 -*-

"""
Line editor implementation.
"""

SEP_LIST = "!@#$%^&*()-+={[}]|\\:;" "<,>.?/"


def _normalize_separator(s: str, seps: str = SEP_LIST) -> str:
    for sep in seps:
        s = s.replace(sep, " ")
    return s


[docs]class LineEditor: """ Simulate a user input line editor. User can type characters, move cursor, backspace, delete, clear line, etc ... For example, the ``(Query): beautiful|`` (``|`` is the cursor) in the following UI is the line editor. .. code-block:: bash (Query): beautiful| [x] Beautiful is better than ugly. subtitle 01 [ ] Explicit is better than implicit. subtitle 02 [ ] Simple is better than complex. subtitle 03 Empty line editor: .. code-block:: bash | User entered some text, and the cursor is at the end: .. code-block:: bash my text| User entered some text, and the cursor is in the middle: .. code-block:: bash my |text :param chars: a list of characters, representing the current line. For example, if ``chars = ["m", "y", " ", "t", "e", "x", "t"]``, then the current line is ``my text``. :param cursor_position: the current cursor position. 0 means the cursor is at the beginning of the line. 1 means it is after the first character. when cursor_position == len(chars), it means the cursor is at the end. For example, if the text is ``my text`` and the ``cursor_position = 5`` Then the cursor is at ``my te|xt``. """ def __init__(self): self.chars = [] self.cursor_position = 0
[docs] def is_cursor_at_begin_of_line(self) -> bool: """ Check if the cursor is at the beginning of the line. """ return self.cursor_position == 0
[docs] def is_cursor_at_end_of_line(self) -> bool: """ Check if the cursor is at the end of the line. """ return self.cursor_position == len(self.chars)
[docs] def enter_text(self, text: str): """ Enter text into the line editor. """ for char in text: self.press_key(key=char)
def _press_key(self, key: str): if self.is_cursor_at_end_of_line(): self.chars.append(key) self.cursor_position += 1 else: self.chars.insert(self.cursor_position, key) self.cursor_position += 1
[docs] def press_key(self, key: str, n: int = 1): """ Enter a key into the line editor. Also move cursor to the right. :param key: the entered character of the key. :param n: number of times to enter the key. """ for _ in range(n): self._press_key(key)
def _press_backspace(self): if self.cursor_position == 0: pass elif self.cursor_position == len(self.chars): self.chars.pop() self.cursor_position -= 1 else: self.cursor_position -= 1 self.chars.pop(self.cursor_position)
[docs] def press_backspace(self, n: int = 1): """ Delete character backwards in the line editor. Also move cursor to the left. :param n: number of characters to delete. """ for _ in range(n): self._press_backspace()
def _press_left(self): if self.cursor_position != 0: self.cursor_position -= 1
[docs] def press_left(self, n: int = 1): """ Move cursor to the left. :param n: number of times to move cursor to the left. """ for _ in range(n): self._press_left()
[docs] def press_home(self): """ Move cursor to the beginning of the line. """ self.cursor_position = 0
def _press_delete(self): if self.cursor_position == len(self.chars): pass else: self.chars.pop(self.cursor_position)
[docs] def press_delete(self, n: int = 1): """ Delete character forwards in the line editor. Also, the cursor stays. :param n: number of characters to delete. """ for _ in range(n): self._press_delete()
def _press_right(self): if self.cursor_position != len(self.chars): self.cursor_position += 1
[docs] def press_right(self, n: int = 1): """ Move cursor to the right. :param n: number of times to move cursor to the right. """ for _ in range(n): self._press_right()
[docs] def press_end(self): """ Move cursor to the end of the line. """ self.cursor_position = len(self.chars)
[docs] def clear_line(self): """ Delete all user inputs and move cursor to the beginning of the line. """ self.chars.clear() self.cursor_position = 0
[docs] def clear_backward(self): """ Delete all user inputs before the cursor and move cursor to the beginning of the line. """ self.chars = self.chars[self.cursor_position :] self.cursor_position = 0
[docs] def clear_forward(self): """ Delete all user inputs after the cursor and the cursor stays. """ self.chars = self.chars[: self.cursor_position] self.cursor_position = len(self.chars)
[docs] def replace_text(self, text: str): """ Replace all user inputs with the given text. :param text: the text to replace with. """ self.clear_line() self.enter_text(text)
move_to_start = press_home move_to_end = press_end def _locate_previous_word_position(self) -> int: """ Locate the cursor position of the beginning of previous word. """ # 先获得光标之前的字符串 line = self.value # 按照空格分割开, words 里面的元素可以是空字符串 words = _normalize_separator(line).split(" ") # print(f"before: words = {words}") # 从后往前找到第一个非空字符串的 index ind = None for i, word in enumerate(words[::-1]): if word: ind = i break # print(f"ind = {ind}") # 如果找到了非空字符串 if ind is not None: # 那么保留所有非空字符串之前的 word, 并把最后一个非空字符串替换成空字符串 # 这样即可算的 cursor position if ind: words = words[:-ind] words[-1] = "" # print(f"after: words = {words}") return len(" ".join(words)) # 如果找不到非空字符串, 则移动到行首 else: return 0
[docs] def move_word_backward(self): """ Move cursor to the beginning of previous word. """ self.cursor_position = self._locate_previous_word_position()
[docs] def delete_word_backward(self): """ Delete the previous word. """ delta = self.cursor_position - self._locate_previous_word_position() self.press_backspace(delta)
def _locate_next_word_position(self) -> int: """ Locate the cursor position of the beginning of next word. """ # 先获得光标之后的字符串 line = "".join(self.chars[self.cursor_position :]) # 按照空格分割开, words 里面的元素可以是空字符串 words = _normalize_separator(line).split(" ") # print(f"before: words = {words}") # 从前往后找到第一个非空字符串 ind = None for i, word in enumerate(words): if word: ind = i break # print(f"ind = {ind}") # 如果找到了非空字符串, 则计算这个非空字符串起之前的所有字符串的总长度 # 这个长度就是 cursor 要移动的距离 if ind is not None: words = words[: (ind + 1)] # print(f"after: words = {words}") return self.cursor_position + len(" ".join(words)) # 如果找不到非空字符串, 则移动到行尾 else: return len(self.chars)
[docs] def move_word_forward(self): """ Move cursor to the beginning of next word. """ self.cursor_position = self._locate_next_word_position()
[docs] def delete_word_forward(self): """ Delete the next word. """ delta = self._locate_next_word_position() - self.cursor_position self.press_delete(delta)
@property def line(self) -> str: """ Return the displayed line. Example: ``ali|ce`` -> line = alice """ return "".join(self.chars) @property def value(self) -> str: """ The value of the user input, it is the text before the cursor. Example: ``ali|ce`` -> value = ali """ return "".join(self.chars[: self.cursor_position])