Skip to content

Commit b23e480

Browse files
Merge branch 'reverse-incremental-search'
2 parents f8ccef3 + d86d8a8 commit b23e480

File tree

4 files changed

+121
-42
lines changed

4 files changed

+121
-42
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: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -298,16 +298,20 @@ 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'
310+
self.incremental_search_target = ''
307311

308312
self.original_modules = sys.modules.keys()
309313

310-
self.width = None # will both be set by a window resize event
314+
self.width = None
311315
self.height = None
312316

313317
self.status_bar.message(banner)
@@ -460,6 +464,12 @@ def process_key_event(self, e):
460464
self.down_one_line()
461465
elif e in ("<Ctrl-d>",):
462466
self.on_control_d()
467+
elif e in ("<Esc+r>",):
468+
self.incremental_search(reverse=True)
469+
elif e in ("<Esc+s>",):
470+
self.incremental_search()
471+
elif e in ("<BACKSPACE>", '<Ctrl-h>') and self.special_mode:
472+
self.add_to_incremental_search(self, backspace=True)
463473
elif e in self.edit_keys.cut_buffer_edits:
464474
self.readline_kill(e)
465475
elif e in self.edit_keys.simple_edits:
@@ -503,12 +513,34 @@ def process_key_event(self, e):
503513
elif e in key_dispatch[self.config.edit_current_block_key]:
504514
self.send_current_block_to_external_editor()
505515
elif e in ["<ESC>"]: #ESC
506-
pass
516+
self.special_mode = None
507517
elif e in ["<SPACE>"]:
508518
self.add_normal_character(' ')
509519
else:
510520
self.add_normal_character(e)
511521

522+
def incremental_search(self, reverse=False, include_current=False):
523+
if self.special_mode == None:
524+
self.rl_history.enter(self.current_line)
525+
self.incremental_search_target = ''
526+
else:
527+
if self.incremental_search_target:
528+
line = (self.rl_history.back(False, search=True,
529+
target=self.incremental_search_target,
530+
include_current=include_current)
531+
if reverse else
532+
self.rl_history.forward(False, search=True,
533+
target=self.incremental_search_target,
534+
include_current=include_current))
535+
self._set_current_line(line,
536+
reset_rl_history=False, clear_special_mode=False)
537+
self._set_cursor_offset(len(self.current_line),
538+
reset_rl_history=False, clear_special_mode=False)
539+
if reverse:
540+
self.special_mode = 'reverse_incremental_search'
541+
else:
542+
self.special_mode = 'incremental_search'
543+
512544
def readline_kill(self, e):
513545
func = self.edit_keys[e]
514546
self.cursor_offset, self.current_line, cut = func(self.cursor_offset, self.current_line)
@@ -661,14 +693,35 @@ def toggle_file_watch(self):
661693
def add_normal_character(self, char):
662694
if len(char) > 1 or is_nop(char):
663695
return
664-
self.current_line = (self.current_line[:self.cursor_offset] +
665-
char +
666-
self.current_line[self.cursor_offset:])
667-
self.cursor_offset += 1
696+
if self.special_mode:
697+
self.add_to_incremental_search(char)
698+
else:
699+
self.current_line = (self.current_line[:self.cursor_offset] +
700+
char +
701+
self.current_line[self.cursor_offset:])
702+
self.cursor_offset += 1
668703
if self.config.cli_trim_prompts and self.current_line.startswith(self.ps1):
669704
self.current_line = self.current_line[4:]
670705
self.cursor_offset = max(0, self.cursor_offset - 4)
671706

707+
def add_to_incremental_search(self, char=None, backspace=False):
708+
"""Modify the current search term while in incremental search.
709+
710+
The only operations allowed in incremental search mode are
711+
adding characters and backspacing."""
712+
if char is None and not backspace:
713+
raise ValueError("must provide a char or set backspace to True")
714+
if backspace:
715+
self.incremental_search_target = self.incremental_search_target[:-1]
716+
else:
717+
self.incremental_search_target += char
718+
if self.special_mode == 'reverse_incremental_search':
719+
self.incremental_search(reverse=True, include_current=True)
720+
elif self.special_mode == 'incremental_search':
721+
self.incremental_search(include_current=True)
722+
else:
723+
raise ValueError('add_to_incremental_search should only be called in a special mode')
724+
672725
def update_completion(self, tab=False):
673726
"""Update visible docstring and matches, and possibly hide/show completion box"""
674727
#Update autocomplete info; self.matches_iter and self.argspec
@@ -837,7 +890,10 @@ def current_line_formatted(self):
837890
"""The colored current line (no prompt, not wrapped)"""
838891
if self.config.syntax:
839892
fs = bpythonparse(format(self.tokenize(self.current_line), self.formatter))
840-
if self.rl_history.saved_line in self.current_line:
893+
if self.special_mode:
894+
if self.incremental_search_target in self.current_line:
895+
fs = fmtfuncs.on_magenta(self.incremental_search_target).join(fs.split(self.incremental_search_target))
896+
elif self.rl_history.saved_line and self.rl_history.saved_line in self.current_line:
841897
if self.config.curtsies_right_arrow_completion:
842898
fs = fmtfuncs.on_magenta(self.rl_history.saved_line).join(fs.split(self.rl_history.saved_line))
843899
logger.debug('Display line %r -> %r', self.current_line, fs)
@@ -868,6 +924,12 @@ def display_buffer_lines(self):
868924
@property
869925
def display_line_with_prompt(self):
870926
"""colored line with prompt"""
927+
if self.special_mode == 'reverse_incremental_search':
928+
return func_for_letter(self.config.color_scheme['prompt'])(
929+
'(reverse-i-search)`%s\': ' % (self.incremental_search_target,)) + self.current_line_formatted
930+
elif self.special_mode == 'incremental_search':
931+
return func_for_letter(self.config.color_scheme['prompt'])(
932+
'(i-search)`%s\': ' % (self.incremental_search_target,)) + self.current_line_formatted
871933
return (func_for_letter(self.config.color_scheme['prompt'])(self.ps1)
872934
if self.done else
873935
func_for_letter(self.config.color_scheme['prompt_more'])(self.ps2)) + self.current_line_formatted
@@ -1087,21 +1149,25 @@ def __repr__(self):
10871149

10881150
def _get_current_line(self):
10891151
return self._current_line
1090-
def _set_current_line(self, line, update_completion=True, reset_rl_history=True):
1152+
def _set_current_line(self, line, update_completion=True, reset_rl_history=True, clear_special_mode=True):
10911153
self._current_line = line
10921154
if update_completion:
10931155
self.update_completion()
10941156
if reset_rl_history:
10951157
self.rl_history.reset()
1158+
if clear_special_mode:
1159+
self.special_mode = None
10961160
current_line = property(_get_current_line, _set_current_line, None,
10971161
"The current line")
10981162
def _get_cursor_offset(self):
10991163
return self._cursor_offset
1100-
def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=True):
1164+
def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=True, clear_special_mode=True):
11011165
if update_completion:
11021166
self.update_completion()
11031167
if reset_rl_history:
11041168
self.rl_history.reset()
1169+
if clear_special_mode:
1170+
self.special_mode = None
11051171
self._cursor_offset = offset
11061172
self.update_completion()
11071173
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,

bpython/repl.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,16 @@ def writetb(self, lines):
139139

140140

141141
class History(object):
142+
"""Stores readline-style history and current place in it"""
142143

143144
def __init__(self, entries=None, duplicates=False):
144145
if entries is None:
145146
self.entries = ['']
146147
else:
147148
self.entries = list(entries)
148-
self.index = 0
149-
self.saved_line = ''
149+
self.index = 0 # how many lines back in history is currently selected
150+
# where 0 is the saved typed line, 1 the prev entered line
151+
self.saved_line = '' # what was on the prompt before using history
150152
self.duplicates = duplicates
151153

152154
def append(self, line):
@@ -167,58 +169,68 @@ def first(self):
167169
self.index = len(self.entries)
168170
return self.entries[-self.index]
169171

170-
def back(self, start=True, search=False):
172+
def back(self, start=True, search=False, target=None, include_current=False):
171173
"""Move one step back in the history."""
174+
if target is None:
175+
target = self.saved_line
172176
if not self.is_at_end:
173177
if search:
174-
self.index += self.find_partial_match_backward(self.saved_line)
178+
self.index += self.find_partial_match_backward(target, include_current)
175179
elif start:
176-
self.index += self.find_match_backward(self.saved_line)
180+
self.index += self.find_match_backward(target, include_current)
177181
else:
178182
self.index += 1
183+
return self.entry
184+
185+
@property
186+
def entry(self):
187+
"""The current entry, which may be the saved line"""
179188
return self.entries[-self.index] if self.index else self.saved_line
180189

181-
def find_match_backward(self, search_term):
182-
filtered_list_len = len(self.entries) - self.index
183-
for idx, val in enumerate(reversed(self.entries[:filtered_list_len])):
190+
@property
191+
def entries_by_index(self):
192+
return list(reversed(self.entries + [self.saved_line]))
193+
194+
def find_match_backward(self, search_term, include_current=False):
195+
for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
184196
if val.startswith(search_term):
185-
return idx + 1
197+
return idx + (0 if include_current else 1)
186198
return 0
187199

188-
def find_partial_match_backward(self, search_term):
189-
filtered_list_len = len(self.entries) - self.index
190-
for idx, val in enumerate(reversed(self.entries[:filtered_list_len])):
200+
def find_partial_match_backward(self, search_term, include_current=False):
201+
for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
191202
if search_term in val:
192-
return idx + 1
203+
return idx + (0 if include_current else 1)
193204
return 0
194205

195206

196-
def forward(self, start=True, search=False):
207+
def forward(self, start=True, search=False, target=None, include_current=False):
197208
"""Move one step forward in the history."""
209+
if target is None:
210+
target = self.saved_line
198211
if self.index > 1:
199212
if search:
200-
self.index -= self.find_partial_match_forward(self.saved_line)
213+
self.index -= self.find_partial_match_forward(target, include_current)
201214
elif start:
202-
self.index -= self.find_match_forward(self.saved_line)
215+
self.index -= self.find_match_forward(target, include_current)
203216
else:
204217
self.index -= 1
205-
return self.entries[-self.index] if self.index else self.saved_line
218+
return self.entry
206219
else:
207220
self.index = 0
208221
return self.saved_line
209222

210-
def find_match_forward(self, search_term):
211-
filtered_list_len = len(self.entries) - self.index + 1
212-
for idx, val in enumerate(self.entries[filtered_list_len:]):
223+
def find_match_forward(self, search_term, include_current=False):
224+
#TODO these are no longer efficient, because we realize the whole list. Does this matter?
225+
for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
213226
if val.startswith(search_term):
214-
return idx + 1
227+
return idx + (0 if include_current else 1)
215228
return self.index
216229

217-
def find_partial_match_forward(self, search_term):
218-
filtered_list_len = len(self.entries) - self.index + 1
219-
for idx, val in enumerate(self.entries[filtered_list_len:]):
230+
def find_partial_match_forward(self, search_term, include_current=False):
231+
for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
220232
if search_term in val:
221-
return idx + 1
233+
return idx + (0 if include_current else 1)
222234
return self.index
223235

224236

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)