Implement ^R reverse search for history
This commit is contained in:
81
babi.py
81
babi.py
@@ -259,29 +259,43 @@ class Status:
|
||||
*,
|
||||
history: Optional[str] = None,
|
||||
) -> str:
|
||||
self.clear()
|
||||
if history is not None:
|
||||
lst = [*self._history[history], '']
|
||||
lst_pos = len(lst) - 1
|
||||
else:
|
||||
lst = ['']
|
||||
lst_pos = 0
|
||||
|
||||
self.clear()
|
||||
pos = 0
|
||||
while True:
|
||||
if not prompt or curses.COLS < 7:
|
||||
prompt_s = ''
|
||||
elif len(prompt) > curses.COLS - 6:
|
||||
prompt_s = f'{prompt[:curses.COLS - 7]}…: '
|
||||
else:
|
||||
prompt_s = f'{prompt}: '
|
||||
|
||||
def buf() -> str:
|
||||
return lst[lst_pos]
|
||||
|
||||
def set_buf(s: str) -> None:
|
||||
lst[lst_pos] = s
|
||||
|
||||
def _save_history_entry() -> None:
|
||||
if history is not None:
|
||||
history_lst = self._history[history]
|
||||
if not history_lst or history_lst[-1] != lst[lst_pos]:
|
||||
history_lst.append(lst[lst_pos])
|
||||
|
||||
def _render_prompt(*, base: str = prompt) -> None:
|
||||
if not base or curses.COLS < 7:
|
||||
prompt_s = ''
|
||||
elif len(base) > curses.COLS - 6:
|
||||
prompt_s = f'{base[:curses.COLS - 7]}…: '
|
||||
else:
|
||||
prompt_s = f'{base}: '
|
||||
width = curses.COLS - len(prompt_s)
|
||||
line = _scrolled_line(lst[lst_pos], pos, width, current=True)
|
||||
cmd = f'{prompt_s}{line}'
|
||||
screen.stdscr.insstr(curses.LINES - 1, 0, cmd, curses.A_REVERSE)
|
||||
line_x = _line_x(pos, width)
|
||||
screen.stdscr.move(curses.LINES - 1, len(prompt_s) + pos - line_x)
|
||||
|
||||
while True:
|
||||
_render_prompt()
|
||||
key = _get_char(screen.stdscr)
|
||||
|
||||
if key.key == curses.KEY_RESIZE:
|
||||
@@ -302,22 +316,55 @@ class Status:
|
||||
pos = len(lst[lst_pos])
|
||||
elif key.key == curses.KEY_BACKSPACE:
|
||||
if pos > 0:
|
||||
lst[lst_pos] = lst[lst_pos][:pos - 1] + lst[lst_pos][pos:]
|
||||
set_buf(buf()[:pos - 1] + buf()[pos:])
|
||||
pos -= 1
|
||||
elif key.key == curses.KEY_DC:
|
||||
if pos < len(lst[lst_pos]):
|
||||
lst[lst_pos] = lst[lst_pos][:pos] + lst[lst_pos][pos + 1:]
|
||||
set_buf(buf()[:pos] + buf()[pos + 1:])
|
||||
elif isinstance(key.wch, str) and key.wch.isprintable():
|
||||
c = key.wch
|
||||
lst[lst_pos] = lst[lst_pos][:pos] + c + lst[lst_pos][pos:]
|
||||
set_buf(buf()[:pos] + key.wch + buf()[pos:])
|
||||
pos += 1
|
||||
elif key.keyname == b'^R':
|
||||
reverse_s = ''
|
||||
reverse_idx = lst_pos
|
||||
while True:
|
||||
reverse_failed = False
|
||||
for search_idx in range(reverse_idx, -1, -1):
|
||||
if reverse_s in lst[search_idx]:
|
||||
reverse_idx = lst_pos = search_idx
|
||||
pos = len(buf())
|
||||
break
|
||||
else:
|
||||
reverse_failed = True
|
||||
|
||||
if reverse_failed:
|
||||
base = f'{prompt}(failed reverse-search)`{reverse_s}`'
|
||||
else:
|
||||
base = f'{prompt}(reverse-search)`{reverse_s}`'
|
||||
|
||||
_render_prompt(base=base)
|
||||
key = _get_char(screen.stdscr)
|
||||
|
||||
if key.key == curses.KEY_RESIZE:
|
||||
screen.resize()
|
||||
elif key.key == curses.KEY_BACKSPACE:
|
||||
reverse_s = reverse_s[:-1]
|
||||
elif isinstance(key.wch, str) and key.wch.isprintable():
|
||||
reverse_s += key.wch
|
||||
elif key.keyname == b'^R':
|
||||
reverse_idx = max(0, reverse_idx - 1)
|
||||
elif key.keyname == b'^C':
|
||||
return ''
|
||||
elif key.key == ord('\r'):
|
||||
_save_history_entry()
|
||||
return lst[lst_pos]
|
||||
else:
|
||||
break
|
||||
|
||||
elif key.keyname == b'^C':
|
||||
return ''
|
||||
elif key.key == ord('\r'):
|
||||
if history is not None:
|
||||
history_lst = self._history[history]
|
||||
if not history_lst or history_lst[-1] != lst[lst_pos]:
|
||||
history_lst.append(lst[lst_pos])
|
||||
_save_history_entry()
|
||||
return lst[lst_pos]
|
||||
|
||||
|
||||
|
||||
@@ -784,6 +784,118 @@ def test_search_history_is_saved_between_sessions(xdg_data_home):
|
||||
h.press('Enter')
|
||||
|
||||
|
||||
def test_search_reverse_search_history(xdg_data_home, ten_lines):
|
||||
xdg_data_home.join('babi/history/search').ensure().write(
|
||||
'line_5\n'
|
||||
'line_3\n'
|
||||
'line_1\n',
|
||||
)
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('^W')
|
||||
h.press('^R')
|
||||
h.await_text('search(reverse-search)``:')
|
||||
h.press('line')
|
||||
h.await_text('search(reverse-search)`line`: line_1')
|
||||
h.press('a')
|
||||
h.await_text('search(failed reverse-search)`linea`: line_1')
|
||||
h.press('BSpace')
|
||||
h.await_text('search(reverse-search)`line`: line_1')
|
||||
h.press('^R')
|
||||
h.await_text('search(reverse-search)`line`: line_3')
|
||||
h.press('Enter')
|
||||
h.await_cursor_position(x=0, y=4)
|
||||
|
||||
|
||||
def test_search_reverse_search_history_pos_after(xdg_data_home, ten_lines):
|
||||
xdg_data_home.join('babi/history/search').ensure().write(
|
||||
'line_3\n',
|
||||
)
|
||||
with run(str(ten_lines), height=20) as h, and_exit(h):
|
||||
h.press('^W')
|
||||
h.press('^R')
|
||||
h.press('line')
|
||||
h.await_text('search(reverse-search)`line`: line_3')
|
||||
h.press('Right')
|
||||
h.await_text('search: line_3')
|
||||
h.await_cursor_position(y=19, x=14)
|
||||
h.press('^C')
|
||||
|
||||
|
||||
def test_search_reverse_search_enter_saves_entry(xdg_data_home, ten_lines):
|
||||
xdg_data_home.join('babi/history/search').ensure().write(
|
||||
'line_1\n'
|
||||
'line_3\n',
|
||||
)
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('^W')
|
||||
h.press('^R')
|
||||
h.press('1')
|
||||
h.await_text('search(reverse-search)`1`: line_1')
|
||||
h.press('Enter')
|
||||
h.press('^W')
|
||||
h.press('Up')
|
||||
h.await_text('search: line_1')
|
||||
h.press('^C')
|
||||
|
||||
|
||||
def test_search_reverse_search_history_cancel():
|
||||
with run() as h, and_exit(h):
|
||||
h.press('^W')
|
||||
h.press('^R')
|
||||
h.await_text('search(reverse-search)``:')
|
||||
h.press('^C')
|
||||
h.await_text('cancelled')
|
||||
|
||||
|
||||
def test_search_reverse_search_resizing():
|
||||
with run() as h, and_exit(h):
|
||||
h.press('^W')
|
||||
h.press('^R')
|
||||
with h.resize(width=24, height=24):
|
||||
h.await_text('search(reverse-se…:')
|
||||
h.press('^C')
|
||||
|
||||
|
||||
def test_search_reverse_search_does_not_wrap_around(xdg_data_home):
|
||||
xdg_data_home.join('babi/history/search').ensure().write(
|
||||
'line_1\n'
|
||||
'line_3\n',
|
||||
)
|
||||
with run() as h, and_exit(h):
|
||||
h.press('^W')
|
||||
h.press('^R')
|
||||
# this should not wrap around
|
||||
for i in range(6):
|
||||
h.press('^R')
|
||||
h.await_text('search(reverse-search)``: line_1')
|
||||
h.press('^C')
|
||||
|
||||
|
||||
def test_search_reverse_search_ctrl_r_on_failed_match(xdg_data_home):
|
||||
xdg_data_home.join('babi/history/search').ensure().write(
|
||||
'nomatch\n'
|
||||
'line_1\n',
|
||||
)
|
||||
with run() as h, and_exit(h):
|
||||
h.press('^W')
|
||||
h.press('^R')
|
||||
h.press('line')
|
||||
h.await_text('search(reverse-search)`line`: line_1')
|
||||
h.press('^R')
|
||||
h.await_text('search(failed reverse-search)`line`: line_1')
|
||||
h.press('^C')
|
||||
|
||||
|
||||
def test_search_reverse_search_keeps_current_text_displayed():
|
||||
with run() as h, and_exit(h):
|
||||
h.press('^W')
|
||||
h.press('ohai')
|
||||
h.await_text('search: ohai')
|
||||
h.press('^R')
|
||||
h.await_text('search(reverse-search)``: ohai')
|
||||
h.press('^C')
|
||||
|
||||
|
||||
def test_scrolling_arrow_key_movement(ten_lines):
|
||||
with run(str(ten_lines), height=10) as h, and_exit(h):
|
||||
h.await_text('line_7')
|
||||
|
||||
Reference in New Issue
Block a user