Skip to content

Commit e318b6b

Browse files
first draft of incremental search (ctrl-r/s)
1 parent fc1cf63 commit e318b6b

File tree

3 files changed

+68
-15
lines changed

3 files changed

+68
-15
lines changed

bpython/curtsiesfrontend/interaction.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def has_focus(self):
5151
return self.in_prompt or self.in_confirm or self.waiting_for_refresh
5252

5353
def message(self, msg):
54+
"""Sets a temporary message"""
5455
self.message_start_time = time.time()
5556
self._message = msg
5657
self.refresh_request(time.time() + self.message_time)

bpython/curtsiesfrontend/repl.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -298,16 +298,19 @@ def smarter_request_reload(desc):
298298
self.stdin = FakeStdin(self.coderunner, self, self.edit_keys)
299299

300300
self.request_paint_to_clear_screen = False # next paint should clear screen
301-
self.last_events = [None] * 50
302-
self.presentation_mode = False
303-
self.paste_mode = False
304-
self.current_match = None
305-
self.list_win_visible = False
306-
self.watching_files = False
301+
self.last_events = [None] * 50 # some commands act differently based on the prev event
302+
# this list doesn't include instances of event.Event,
303+
# only keypress-type events (no refresh screen events etc.)
304+
self.presentation_mode = False # displays prev events in a column on the right hand side
305+
self.paste_mode = False # currently processing a paste event
306+
self.current_match = None # currently tab-selected autocompletion suggestion
307+
self.list_win_visible = False # whether the infobox (suggestions, docstring) is visible
308+
self.watching_files = False # auto reloading turned on
309+
self.special_mode = None # 'reverse_incremental_search' and 'incremental_search'
307310

308311
self.original_modules = sys.modules.keys()
309312

310-
self.width = None # will both be set by a window resize event
313+
self.width = None
311314
self.height = None
312315

313316
self.status_bar.message(banner)
@@ -460,6 +463,12 @@ def process_key_event(self, e):
460463
self.down_one_line()
461464
elif e in ("<Ctrl-d>",):
462465
self.on_control_d()
466+
elif e in ("<Ctrl-r>",):
467+
self.incremental_search(reverse=True)
468+
elif e in ("<Ctrl-s>",):
469+
self.incremental_search()
470+
elif e in ("<BACKSPACE>", '<Ctrl-h>') and self.special_mode:
471+
self.add_to_incremental_search(self, backspace=True)
463472
elif e in self.edit_keys.cut_buffer_edits:
464473
self.readline_kill(e)
465474
elif e in self.edit_keys.simple_edits:
@@ -507,6 +516,21 @@ def process_key_event(self, e):
507516
else:
508517
self.add_normal_character(e)
509518

519+
def incremental_search(self, reverse=False):
520+
if self.special_mode == None:
521+
current_line = ''
522+
if reverse:
523+
self.special_mode = 'reverse_incremental_search'
524+
else:
525+
self.special_mode = 'incremental_search'
526+
else:
527+
self._set_current_line(self.rl_history.back(False, search=True)
528+
if reverse else
529+
self.rl_history.forward(False, search=True),
530+
reset_rl_history=False, clear_special_mode=False)
531+
self._set_cursor_offset(len(self.current_line), reset_rl_history=False,
532+
clear_special_mode=False)
533+
510534
def readline_kill(self, e):
511535
func = self.edit_keys[e]
512536
self.cursor_offset, self.current_line, cut = func(self.cursor_offset, self.current_line)
@@ -659,14 +683,35 @@ def toggle_file_watch(self):
659683
def add_normal_character(self, char):
660684
if len(char) > 1 or is_nop(char):
661685
return
662-
self.current_line = (self.current_line[:self.cursor_offset] +
663-
char +
664-
self.current_line[self.cursor_offset:])
665-
self.cursor_offset += 1
686+
if self.special_mode == 'reverse_incremental_search':
687+
self.add_to_incremental_search(char)
688+
else:
689+
self.current_line = (self.current_line[:self.cursor_offset] +
690+
char +
691+
self.current_line[self.cursor_offset:])
692+
self.cursor_offset += 1
666693
if self.config.cli_trim_prompts and self.current_line.startswith(self.ps1):
667694
self.current_line = self.current_line[4:]
668695
self.cursor_offset = max(0, self.cursor_offset - 4)
669696

697+
def add_to_incremental_search(self, char=None, backspace=False):
698+
if char is None and not backspace:
699+
raise ValueError("must provide a char or set backspace to True")
700+
saved_line = self.rl_history.saved_line
701+
if backspace:
702+
saved_line = saved_line[:-1]
703+
else:
704+
saved_line += char
705+
self.update_completion()
706+
self.rl_history.reset()
707+
self.rl_history.enter(saved_line)
708+
if self.special_mode == 'reverse_incremental_search':
709+
self.incremental_search(reverse=True)
710+
elif self.special_mode == 'incremental_search':
711+
self.incremental_search()
712+
else:
713+
raise ValueError('add_to_incremental_search should only be called in a special mode')
714+
670715
def update_completion(self, tab=False):
671716
"""Update visible docstring and matches, and possibly hide/show completion box"""
672717
#Update autocomplete info; self.matches_iter and self.argspec
@@ -866,6 +911,9 @@ def display_buffer_lines(self):
866911
@property
867912
def display_line_with_prompt(self):
868913
"""colored line with prompt"""
914+
if self.special_mode == 'reverse_incremental_search':
915+
return func_for_letter(self.config.color_scheme['prompt'])(
916+
'(reverse-i-search)`%s\': ' % (self.rl_history.saved_line,)) + self.current_line_formatted
869917
return (func_for_letter(self.config.color_scheme['prompt'])(self.ps1)
870918
if self.done else
871919
func_for_letter(self.config.color_scheme['prompt_more'])(self.ps2)) + self.current_line_formatted
@@ -1085,21 +1133,25 @@ def __repr__(self):
10851133

10861134
def _get_current_line(self):
10871135
return self._current_line
1088-
def _set_current_line(self, line, update_completion=True, reset_rl_history=True):
1136+
def _set_current_line(self, line, update_completion=True, reset_rl_history=True, clear_special_mode=True):
10891137
self._current_line = line
10901138
if update_completion:
10911139
self.update_completion()
10921140
if reset_rl_history:
10931141
self.rl_history.reset()
1142+
if clear_special_mode:
1143+
self.special_mode = None
10941144
current_line = property(_get_current_line, _set_current_line, None,
10951145
"The current line")
10961146
def _get_cursor_offset(self):
10971147
return self._cursor_offset
1098-
def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=True):
1148+
def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=True, clear_special_mode=True):
10991149
if update_completion:
11001150
self.update_completion()
11011151
if reset_rl_history:
11021152
self.rl_history.reset()
1153+
if clear_special_mode:
1154+
self.special_mode = None
11031155
self._cursor_offset = offset
11041156
self.update_completion()
11051157
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,

bpython/test/test_curtsies_painting.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ def test_startup(self):
3838

3939
def test_enter_text(self):
4040
[self.repl.add_normal_character(c) for c in '1 + 1']
41-
screen = fsarray([cyan('>>> ') + bold(green('1')+cyan(' ')+
42-
yellow('+') + cyan(' ') + green('1')), cyan('Welcome to')])
41+
screen = fsarray([cyan('>>> ') + bold(blue('1')+cyan(' ')+
42+
yellow('+') + cyan(' ') + green('1')), cyan('welcome')])
4343
self.assert_paint(screen, (0, 9))
4444

4545
def test_run_line(self):

0 commit comments

Comments
 (0)