Skip to content

Commit 4e8c46a

Browse files
Merge branch 'completion-cleanup'
Conflicts: bpython/curtsies.py bpython/curtsiesfrontend/repl.py setup.py
2 parents 8c0e2b6 + 70efe4b commit 4e8c46a

File tree

17 files changed

+1465
-723
lines changed

17 files changed

+1465
-723
lines changed

bpython/autocomplete.py

Lines changed: 332 additions & 103 deletions
Large diffs are not rendered by default.

bpython/cli.py

Lines changed: 59 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,13 @@ def __init__(self, scr, interp, statusbar, config, idle=None):
353353
if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1:
354354
config.cli_suggestion_width = 0.8
355355

356+
def _get_cursor_offset(self):
357+
return len(self.s) - self.cpos
358+
def _set_cursor_offset(self, offset):
359+
self.cpos = len(self.s) - offset
360+
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,
361+
"The cursor offset from the beginning of the line")
362+
356363
def addstr(self, s):
357364
"""Add a string to the current input line and figure out
358365
where it should go, depending on the cursor position."""
@@ -446,34 +453,29 @@ def clear_wrapped_lines(self):
446453
self.scr.clrtoeol()
447454

448455
def complete(self, tab=False):
449-
"""Get Autcomplete list and window."""
450-
if self.paste_mode and self.list_win_visible:
451-
self.scr.touchwin()
456+
"""Get Autcomplete list and window.
452457
458+
Called whenever these should be updated, and called
459+
with tab
460+
"""
453461
if self.paste_mode:
462+
self.scr.touchwin() #TODO necessary?
454463
return
455464

456-
if self.list_win_visible and not self.config.auto_display_list:
457-
self.scr.touchwin()
458-
self.list_win_visible = False
459-
self.matches_iter.update()
460-
return
461-
462-
if self.config.auto_display_list or tab:
463-
self.list_win_visible = repl.Repl.complete(self, tab)
464-
if self.list_win_visible:
465-
try:
466-
self.show_list(self.matches, self.argspec)
467-
except curses.error:
468-
# XXX: This is a massive hack, it will go away when I get
469-
# cusswords into a good enough state that we can start
470-
# using it.
471-
self.list_win.border()
472-
self.list_win.refresh()
473-
self.list_win_visible = False
474-
if not self.list_win_visible:
475-
self.scr.redrawwin()
476-
self.scr.refresh()
465+
list_win_visible = repl.Repl.complete(self, tab)
466+
if list_win_visible:
467+
try:
468+
self.show_list(self.matches_iter.matches, topline=self.argspec, formatter=self.matches_iter.completer.format)
469+
except curses.error:
470+
# XXX: This is a massive hack, it will go away when I get
471+
# cusswords into a good enough state that we can start
472+
# using it.
473+
self.list_win.border()
474+
self.list_win.refresh()
475+
list_win_visible = False
476+
if not list_win_visible:
477+
self.scr.redrawwin()
478+
self.scr.refresh()
477479

478480
def clrtobol(self):
479481
"""Clear from cursor to beginning of line; usual C-u behaviour"""
@@ -488,9 +490,12 @@ def clrtobol(self):
488490
self.scr.redrawwin()
489491
self.scr.refresh()
490492

491-
def current_line(self):
492-
"""Return the current line."""
493+
def _get_current_line(self):
493494
return self.s
495+
def _set_current_line(self, line):
496+
self.s = line
497+
current_line = property(_get_current_line, _set_current_line, None,
498+
"The characters of the current line")
494499

495500
def cut_to_buffer(self):
496501
"""Clear from cursor to end of line, placing into cut buffer"""
@@ -501,31 +506,6 @@ def cut_to_buffer(self):
501506
self.scr.redrawwin()
502507
self.scr.refresh()
503508

504-
def cw(self):
505-
"""Return the current word, i.e. the (incomplete) word directly to the
506-
left of the cursor"""
507-
508-
# I don't know if autocomplete should be disabled if the cursor
509-
# isn't at the end of the line, but that's what this does for now.
510-
if self.cpos: return
511-
512-
# look from right to left for a bad method or dictionary character
513-
l = len(self.s)
514-
is_method_char = lambda c: c.isalnum() or c in ('.', '_')
515-
dict_chars = ['[']
516-
517-
if not self.s or not (is_method_char(self.s[-1])
518-
or self.s[-1] in dict_chars):
519-
return
520-
521-
for i in range(1, l+1):
522-
c = self.s[-i]
523-
if not (is_method_char(c) or c in dict_chars):
524-
i -= 1
525-
break
526-
527-
return self.s[-i:]
528-
529509
def delete(self):
530510
"""Process a del"""
531511
if not self.s:
@@ -1267,7 +1247,8 @@ def write(self, s):
12671247
self.s_hist.append(s.rstrip())
12681248

12691249

1270-
def show_list(self, items, topline=None, current_item=None):
1250+
def show_list(self, items, topline=None, formatter=None, current_item=None):
1251+
12711252
shared = Struct()
12721253
shared.cols = 0
12731254
shared.rows = 0
@@ -1283,20 +1264,9 @@ def show_list(self, items, topline=None, current_item=None):
12831264
self.list_win.erase()
12841265

12851266
if items:
1286-
sep = '.'
1287-
separators = ['.', os.path.sep, '[']
1288-
lastindex = max([items[0].rfind(c) for c in separators])
1289-
if lastindex > -1:
1290-
sep = items[0][lastindex]
1291-
items = [x.rstrip(sep).rsplit(sep)[-1] for x in items]
1267+
items = [formatter(x) for x in items]
12921268
if current_item:
1293-
current_item = current_item.rstrip(sep).rsplit(sep)[-1]
1294-
1295-
if items[0].endswith(']'):
1296-
# dictionary key suggestions
1297-
items = [x.rstrip(']') for x in items]
1298-
if current_item:
1299-
current_item = current_item.rstrip(']')
1269+
current_item = formatter(current_item)
13001270

13011271
if topline:
13021272
height_offset = self.mkargspec(topline, down) + 1
@@ -1445,8 +1415,6 @@ def tab(self, back=False):
14451415
and don't indent if there are only whitespace in the line.
14461416
"""
14471417

1448-
mode = self.config.autocomplete_mode
1449-
14501418
# 1. check if we should add a tab character
14511419
if self.atbol() and not back:
14521420
x_pos = len(self.s) - self.cpos
@@ -1458,66 +1426,39 @@ def tab(self, back=False):
14581426
self.print_line(self.s)
14591427
return True
14601428

1461-
# 2. get the current word
1429+
# 2. run complete() if we aren't already iterating through matches
14621430
if not self.matches_iter:
14631431
self.complete(tab=True)
1464-
if not self.config.auto_display_list and not self.list_win_visible:
1465-
return True
1466-
1467-
cw = self.current_string() or self.cw()
1468-
if not cw:
1469-
return True
1470-
else:
1471-
cw = self.matches_iter.current_word
1432+
self.print_line(self.s)
14721433

14731434
# 3. check to see if we can expand the current word
1474-
cseq = None
1475-
if mode == autocomplete.SUBSTRING:
1476-
if all([len(match.split(cw)) == 2 for match in self.matches]):
1477-
seq = [cw + match.split(cw)[1] for match in self.matches]
1478-
cseq = os.path.commonprefix(seq)
1479-
else:
1480-
seq = self.matches
1481-
cseq = os.path.commonprefix(seq)
1482-
1483-
if cseq and mode != autocomplete.FUZZY:
1484-
expanded_string = cseq[len(cw):]
1485-
self.s += expanded_string
1486-
expanded = bool(expanded_string)
1435+
if self.matches_iter.is_cseq():
1436+
#TODO resolve this error-prone situation:
1437+
# can't assign at same time to self.s and self.cursor_offset
1438+
# because for cursor_offset
1439+
# property to work correctly, self.s must already be set
1440+
temp_cursor_offset, self.s = self.matches_iter.substitute_cseq()
1441+
self.cursor_offset = temp_cursor_offset
14871442
self.print_line(self.s)
1488-
if len(self.matches) == 1 and self.config.auto_display_list:
1489-
self.scr.touchwin()
1490-
if expanded:
1491-
self.matches_iter.update(cseq, self.matches)
1492-
else:
1493-
expanded = False
1443+
if not self.matches_iter:
1444+
self.complete()
14941445

14951446
# 4. swap current word for a match list item
1496-
if not expanded and self.matches:
1497-
# reset s if this is the nth result
1498-
if self.matches_iter:
1499-
self.s = self.s[:-len(self.matches_iter.current())] + cw
1500-
1447+
elif self.matches_iter.matches:
15011448
current_match = back and self.matches_iter.previous() \
15021449
or self.matches_iter.next()
1503-
1504-
# update s with the new match
1505-
if current_match:
1506-
try:
1507-
self.show_list(self.matches, self.argspec, current_match)
1508-
except curses.error:
1509-
# XXX: This is a massive hack, it will go away when I get
1510-
# cusswords into a good enough state that we can start
1511-
# using it.
1512-
self.list_win.border()
1513-
self.list_win.refresh()
1514-
1515-
if self.config.autocomplete_mode == autocomplete.SIMPLE:
1516-
self.s += current_match[len(cw):]
1517-
else:
1518-
self.s = self.s[:-len(cw)] + current_match
1519-
1520-
self.print_line(self.s, True)
1450+
try:
1451+
self.show_list(self.matches_iter.matches, topline=self.argspec,
1452+
formatter=self.matches_iter.completer.format,
1453+
current_item=current_match)
1454+
except curses.error:
1455+
# XXX: This is a massive hack, it will go away when I get
1456+
# cusswords into a good enough state that we can start
1457+
# using it.
1458+
self.list_win.border()
1459+
self.list_win.refresh()
1460+
_, self.s = self.matches_iter.cur_line()
1461+
self.print_line(self.s, True)
15211462
return True
15221463

15231464
def undo(self, n=1):

bpython/config.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,6 @@
66
from bpython.keys import cli_key_dispatch as key_dispatch
77
from bpython.autocomplete import SIMPLE as default_completion
88

9-
MAGIC_METHODS = ", ".join("__%s__" % s for s in [
10-
"init", "repr", "str", "lt", "le", "eq", "ne", "gt", "ge", "cmp", "hash",
11-
"nonzero", "unicode", "getattr", "setattr", "get", "set","call", "len",
12-
"getitem", "setitem", "iter", "reversed", "contains", "add", "sub", "mul",
13-
"floordiv", "mod", "divmod", "pow", "lshift", "rshift", "and", "xor", "or",
14-
"div", "truediv", "neg", "pos", "abs", "invert", "complex", "int", "float",
15-
"oct", "hex", "index", "coerce", "enter", "exit"]
16-
)
17-
189
class Struct(object):
1910
"""Simple class for instantiating objects we can add arbitrary attributes
2011
to and use for various arbitrary things."""
@@ -55,7 +46,6 @@ def loadini(struct, configfile):
5546
'auto_display_list': True,
5647
'color_scheme': 'default',
5748
'complete_magic_methods' : True,
58-
'magic_methods' : MAGIC_METHODS,
5949
'autocomplete_mode': default_completion,
6050
'dedent_after': 1,
6151
'flush_output': True,
@@ -100,6 +90,7 @@ def loadini(struct, configfile):
10090
'curtsies': {
10191
'list_above' : False,
10292
'fill_terminal' : False,
93+
'right_arrow_completion' : True,
10394
}})
10495
if not config.read(config_path):
10596
# No config file. If the user has it in the old place then complain
@@ -155,13 +146,12 @@ def loadini(struct, configfile):
155146

156147
struct.complete_magic_methods = config.getboolean('general',
157148
'complete_magic_methods')
158-
methods = config.get('general', 'magic_methods')
159-
struct.magic_methods = [meth.strip() for meth in methods.split(",")]
160149
struct.autocomplete_mode = config.get('general', 'autocomplete_mode')
161150
struct.save_append_py = config.getboolean('general', 'save_append_py')
162151

163152
struct.curtsies_list_above = config.getboolean('curtsies', 'list_above')
164153
struct.curtsies_fill_terminal = config.getboolean('curtsies', 'fill_terminal')
154+
struct.curtsies_right_arrow_completion = config.getboolean('curtsies', 'right_arrow_completion')
165155

166156
color_scheme_name = config.get('general', 'color_scheme')
167157

bpython/curtsies.py

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,22 @@
44
import code
55
import logging
66
from optparse import Option
7+
from itertools import izip
78

89
import curtsies
910
import curtsies.window
10-
import curtsies.terminal
11+
import curtsies.input
1112
import curtsies.events
12-
Window = curtsies.window.Window
13-
Terminal = curtsies.terminal.Terminal
1413

1514
from bpython.curtsiesfrontend.repl import Repl
1615
from bpython.curtsiesfrontend.coderunner import SystemExitFromCodeGreenlet
1716
from bpython import args as bpargs
1817
from bpython.translations import _
1918
from bpython.importcompletion import find_iterator
2019

20+
repl = None # global for `from bpython.curtsies import repl`
21+
#WARNING Will be a problem if more than one repl is ever instantiated this way
22+
2123
def main(args=None, locals_=None, banner=None):
2224
config, options, exec_args = bpargs.parse(args, (
2325
'scroll options', None, [
@@ -57,39 +59,63 @@ def main(args=None, locals_=None, banner=None):
5759
else:
5860
sys.path.insert(0, '') # expected for interactive sessions (vanilla python does it)
5961

60-
mainloop(config, locals_, banner, interp, paste)
6162

62-
def mainloop(config, locals_, banner, interp=None, paste=None):
63-
with Terminal(paste_mode=True) as tc:
64-
with Window(tc, keep_last_line=True, hide_cursor=False) as term:
63+
mainloop(config, locals_, banner, interp, paste, interactive=(not exec_args))
64+
65+
def mainloop(config, locals_, banner, interp=None, paste=None, interactive=True):
66+
with curtsies.input.Input(keynames='curses', sigint_event=True) as input_generator:
67+
with curtsies.window.CursorAwareWindow(
68+
sys.stdout,
69+
sys.stdin,
70+
keep_last_line=True,
71+
hide_cursor=False) as window:
72+
73+
refresh_requests = []
74+
def request_refresh():
75+
refresh_requests.append(curtsies.events.RefreshRequestEvent())
76+
def event_or_refresh(timeout=None):
77+
while True:
78+
if refresh_requests:
79+
yield refresh_requests.pop()
80+
else:
81+
yield input_generator.send(timeout)
82+
83+
global repl # global for easy introspection `from bpython.curtsies import repl`
6584
with Repl(config=config,
6685
locals_=locals_,
67-
request_refresh=tc.stuff_a_refresh_request,
86+
request_refresh=request_refresh,
87+
get_term_hw=window.get_term_hw,
88+
get_cursor_vertical_diff=window.get_cursor_vertical_diff,
6889
banner=banner,
69-
interp=interp) as repl:
70-
rows, columns = tc.get_screen_size()
71-
repl.width = columns
72-
repl.height = rows
90+
interp=interp,
91+
interactive=interactive,
92+
orig_tcattrs=input_generator.original_stty) as repl:
93+
repl.height, repl.width = window.t.height, window.t.width
7394

7495
def process_event(e):
96+
"""If None is passed in, just paint the screen"""
7597
try:
76-
repl.process_event(e)
98+
if e is not None:
99+
repl.process_event(e)
77100
except (SystemExitFromCodeGreenlet, SystemExit) as err:
78101
array, cursor_pos = repl.paint(about_to_exit=True, user_quit=isinstance(err, SystemExitFromCodeGreenlet))
79-
scrolled = term.render_to_terminal(array, cursor_pos)
102+
scrolled = window.render_to_terminal(array, cursor_pos)
80103
repl.scroll_offset += scrolled
81104
raise
82105
else:
83106
array, cursor_pos = repl.paint()
84-
scrolled = term.render_to_terminal(array, cursor_pos)
107+
scrolled = window.render_to_terminal(array, cursor_pos)
85108
repl.scroll_offset += scrolled
86109

87110
if paste:
88-
repl.process_event(term.get_annotated_event()) #first event will always be a window size set
89111
process_event(paste)
90112

91-
while True:
92-
process_event(term.get_annotated_event(idle=find_iterator))
113+
process_event(None) #priming the pump (do a display before waiting for first event)
114+
for _, e in izip(find_iterator, event_or_refresh(0)):
115+
if e is not None:
116+
process_event(e)
117+
for e in event_or_refresh():
118+
process_event(e)
93119

94120
if __name__ == '__main__':
95121
sys.exit(main())

0 commit comments

Comments
 (0)