X Tutup
Skip to content

Commit 766f8e4

Browse files
filename completion working
New class-based completion system breaking interestingly
1 parent 1a0aa07 commit 766f8e4

File tree

4 files changed

+233
-157
lines changed

4 files changed

+233
-157
lines changed

bpython/autocomplete.py

Lines changed: 115 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
SUBSTRING = 'substring'
4949
FUZZY = 'fuzzy'
5050

51-
def complete(text, namespace=None, config=None):
51+
def attr_complete(text, namespace=None, config=None):
5252
"""Return list of matches """
5353
if namespace is None:
5454
namespace = __main__.__dict__ #TODO figure out if this __main__ still makes sense
@@ -177,49 +177,124 @@ def filename_matches(cs):
177177
matches.append(filename)
178178
return matches
179179

180-
def find_matches(cursor_offset, current_line, locals_, argspec, config, magic_methods):
181-
"""Returns a list of matches and function to use for replacing words on tab"""
180+
def last_part_of_filename(filename):
181+
filename.rstrip(os.sep).rsplit(os.sep)[-1]
182+
if os.sep in filename[:-1]:
183+
return filename[filename.rindex(os.sep, 0, -1)+1:]
184+
else:
185+
return filename
186+
187+
def after_last_dot(name):
188+
return name.rstrip('.').rsplit('.')[-1]
189+
190+
def dict_key_format(filename):
191+
# dictionary key suggestions
192+
#items = [x.rstrip(']') for x in items]
193+
#if current_item:
194+
# current_item = current_item.rstrip(']')
195+
pass
196+
197+
def get_completer(cursor_offset, current_line, locals_, argspec, config, magic_methods):
198+
"""Returns a list of matches and a class for what kind of completion is happening
199+
200+
If no completion type is relevant, returns None, None"""
182201

183202
#TODO use the smarter current_string() in Repl that knows about the buffer
184203
#TODO don't pass in config, pass in the settings themselves
185-
#TODO if importcompletion returns None, that means short circuit return, not
186-
# try something else
187-
if line.current_string(cursor_offset, current_line):
188-
matches = filename_matches(line.current_string(cursor_offset, current_line)[2])
189-
return matches, line.current_string
190-
191-
if line.current_word(cursor_offset, current_line) is None:
192-
return [], None
193-
194-
matches = importcompletion.complete(cursor_offset, current_line)
195-
if matches:
196-
return matches, line.current_word
197-
198-
cw = line.current_word(cursor_offset, current_line)[2]
199-
200-
try:
201-
matches = complete(cw, namespace=locals_, config=config)
202-
except Exception:
203-
# This sucks, but it's either that or list all the exceptions that could
204-
# possibly be raised here, so if anyone wants to do that, feel free to send me
205-
# a patch. XXX: Make sure you raise here if you're debugging the completion
206-
# stuff !
207-
e = True
208-
raise
209-
else:
210-
e = False
204+
205+
matches = ImportCompletion.matches(cursor_offset, current_line)
206+
if matches is not None:
207+
return sorted(set(matches)), ImportCompletion
208+
209+
matches = FilenameCompletion.matches(cursor_offset, current_line)
210+
if matches is not None:
211+
return sorted(set(matches)), FilenameCompletion
212+
213+
matches = AttrCompletion.matches(cursor_offset, current_line, locals_=locals_, config=config)
214+
if matches is not None:
215+
cw = AttrCompletion.locate(cursor_offset, current_line)[2]
211216
matches.extend(magic_methods(cw))
217+
if argspec:
218+
matches.extend(name + '=' for name in argspec[1][0]
219+
if isinstance(name, basestring) and name.startswith(cw))
220+
if py3:
221+
matches.extend(name + '=' for name in argspec[1][4]
222+
if name.startswith(cw))
212223

213-
if not e and argspec:
214-
matches.extend(name + '=' for name in argspec[1][0]
215-
if isinstance(name, basestring) and name.startswith(cw))
216-
if py3:
217-
matches.extend(name + '=' for name in argspec[1][4]
218-
if name.startswith(cw))
224+
# unless the first character is a _ filter out all attributes starting with a _
225+
if not cw.split('.')[-1].startswith('_'):
226+
matches = [match for match in matches
227+
if not match.split('.')[-1].startswith('_')]
219228

220-
# unless the first character is a _ filter out all attributes starting with a _
221-
if not e and not cw.split('.')[-1].startswith('_'):
222-
matches = [match for match in matches
223-
if not match.split('.')[-1].startswith('_')]
229+
return sorted(set(matches)), AttrCompletion
224230

225-
return sorted(set(matches)), line.current_word
231+
return None, None
232+
233+
234+
class BaseCompletionType(object):
235+
"""Describes different completion types"""
236+
def matches(cls, cursor_offset, line):
237+
"""Returns a list of possible matches given a line and cursor, or None
238+
if this completion type isn't applicable.
239+
240+
ie, import completion doesn't make sense if there cursor isn't after
241+
an import or from statement
242+
243+
Completion types are used to:
244+
* `locate(cur, line)` their target word to replace given a line and cursor
245+
* find `matches(cur, line)` that might replace that word
246+
* `format(match)` matches to be displayed to the user
247+
* determine whether suggestions should be `shown_before_tab`
248+
* `substitute(cur, line, match)` in a match for what's found with `target`
249+
"""
250+
raise NotImplementedError
251+
def locate(cls, cursor_offset, line):
252+
"""Returns a start, stop, and word given a line and cursor, or None
253+
if no target for this type of completion is found under the cursor"""
254+
raise NotImplementedError
255+
def format(cls, word):
256+
return word
257+
shown_before_tab = True # whether suggestions should be shown before the
258+
# user hits tab, or only once that has happened
259+
def substitute(cls, cursor_offset, line, match):
260+
"""Returns a cursor offset and line with match swapped in"""
261+
start, end, word = cls.locate(cursor_offset, line)
262+
result = start + len(match), line[:start] + match + line[end:]
263+
return result
264+
265+
class ImportCompletion(BaseCompletionType):
266+
matches = staticmethod(importcompletion.complete)
267+
locate = staticmethod(line.current_word)
268+
format = staticmethod(after_last_dot)
269+
270+
class FilenameCompletion(BaseCompletionType):
271+
shown_before_tab = False
272+
@classmethod
273+
def matches(cls, cursor_offset, current_line):
274+
cs = line.current_string(cursor_offset, current_line)
275+
if cs is None:
276+
return None
277+
return filename_matches(cs[2])
278+
locate = staticmethod(line.current_string)
279+
format = staticmethod(last_part_of_filename)
280+
281+
class AttrCompletion(BaseCompletionType):
282+
@classmethod
283+
def matches(cls, cursor_offset, line, locals_, config):
284+
r = cls.locate(cursor_offset, line)
285+
if r is None:
286+
return None
287+
cw = r[2]
288+
try:
289+
return attr_complete(cw, namespace=locals_, config=config)
290+
except Exception:
291+
# This sucks, but it's either that or list all the exceptions that could
292+
# possibly be raised here, so if anyone wants to do that, feel free to send me
293+
# a patch. XXX: Make sure you raise here if you're debugging the completion
294+
# stuff !
295+
e = True
296+
raise
297+
else:
298+
e = False
299+
locate = staticmethod(line.current_word)
300+
format = staticmethod(after_last_dot)

bpython/cli.py

Lines changed: 21 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,11 @@ def clear_wrapped_lines(self):
449449
self.scr.clrtoeol()
450450

451451
def complete(self, tab=False):
452-
"""Get Autcomplete list and window."""
452+
"""Get Autcomplete list and window.
453+
454+
Called whenever these should be updated, and called
455+
with tab
456+
"""
453457
if self.paste_mode and self.list_win_visible:
454458
self.scr.touchwin()
455459

@@ -466,7 +470,7 @@ def complete(self, tab=False):
466470
self.list_win_visible = repl.Repl.complete(self, tab)
467471
if self.list_win_visible:
468472
try:
469-
self.show_list(self.matches, self.argspec)
473+
self.show_list(self.matches_iter.matches, topline=self.argspec, formatter=self.matches_iter.completer.format)
470474
except curses.error:
471475
# XXX: This is a massive hack, it will go away when I get
472476
# cusswords into a good enough state that we can start
@@ -1270,7 +1274,8 @@ def write(self, s):
12701274
self.s_hist.append(s.rstrip())
12711275

12721276

1273-
def show_list(self, items, topline=None, current_item=None):
1277+
def show_list(self, items, topline=None, formatter=None, current_item=None):
1278+
12741279
shared = Struct()
12751280
shared.cols = 0
12761281
shared.rows = 0
@@ -1286,20 +1291,9 @@ def show_list(self, items, topline=None, current_item=None):
12861291
self.list_win.erase()
12871292

12881293
if items:
1289-
sep = '.'
1290-
separators = ['.', os.path.sep, '[']
1291-
lastindex = max([items[0].rfind(c) for c in separators])
1292-
if lastindex > -1:
1293-
sep = items[0][lastindex]
1294-
items = [x.rstrip(sep).rsplit(sep)[-1] for x in items]
1294+
items = [formatter(x) for x in items]
12951295
if current_item:
1296-
current_item = current_item.rstrip(sep).rsplit(sep)[-1]
1297-
1298-
if items[0].endswith(']'):
1299-
# dictionary key suggestions
1300-
items = [x.rstrip(']') for x in items]
1301-
if current_item:
1302-
current_item = current_item.rstrip(']')
1296+
current_item = formatter(current_item)
13031297

13041298
if topline:
13051299
height_offset = self.mkargspec(topline, down) + 1
@@ -1448,8 +1442,6 @@ def tab(self, back=False):
14481442
and don't indent if there are only whitespace in the line.
14491443
"""
14501444

1451-
mode = self.config.autocomplete_mode
1452-
14531445
# 1. check if we should add a tab character
14541446
if self.atbol() and not back:
14551447
x_pos = len(self.s) - self.cpos
@@ -1461,64 +1453,33 @@ def tab(self, back=False):
14611453
self.print_line(self.s)
14621454
return True
14631455

1464-
# 2. get the current word
1456+
# 2. run complete() if we aren't already iterating through matches
14651457
if not self.matches_iter:
14661458
self.complete(tab=True)
1467-
if not self.config.auto_display_list and not self.list_win_visible:
1468-
return True
1469-
1470-
cw = self.current_string() or self.cw()
1471-
if not cw:
1472-
return True
1473-
else:
1474-
cw = self.matches_iter.current_word
14751459

14761460
# 3. check to see if we can expand the current word
1477-
cseq = None
1478-
if mode == autocomplete.SUBSTRING:
1479-
if all([len(match.split(cw)) == 2 for match in self.matches]):
1480-
seq = [cw + match.split(cw)[1] for match in self.matches]
1481-
cseq = os.path.commonprefix(seq)
1482-
else:
1483-
seq = self.matches
1484-
cseq = os.path.commonprefix(seq)
1485-
1486-
if cseq and mode != autocomplete.FUZZY:
1487-
print 'doing cseq'
1488-
expanded_string = cseq[len(cw):]
1489-
expanded = bool(expanded_string) #TODO move this logic below to matches_iter
1490-
_, self.s = self.matches_iter.substitute(self.matches_iter.current_word + expanded_string)
1461+
if self.matches_iter.is_cseq():
1462+
_, self.s = self.matches_iter.substitute_cseq()
14911463
self.print_line(self.s)
1492-
if len(self.matches) == 1 and self.config.auto_display_list:
1493-
self.scr.touchwin()
1494-
if expanded:
1495-
self.matches_iter.update(len(self.s) - self.cpos,
1496-
self.s,
1497-
self.matches)
1498-
else:
1499-
expanded = False
1464+
if not self.matches_iter and self.config.auto_display_list:
1465+
self.complete()
1466+
#self.scr.touchwin() #TODO necessary?
15001467

15011468
# 4. swap current word for a match list item
1502-
if not expanded and self.matches:
1503-
print 'doing swap'
1504-
1469+
elif self.matches_iter.matches:
15051470
current_match = back and self.matches_iter.previous() \
15061471
or self.matches_iter.next()
1507-
15081472
try:
1509-
self.show_list(self.matches, self.argspec, current_match)
1473+
self.show_list(self.matches_iter.matches, topline=self.argspec,
1474+
formatter=self.matches_iter.completer.format,
1475+
current_item=current_match)
15101476
except curses.error:
15111477
# XXX: This is a massive hack, it will go away when I get
15121478
# cusswords into a good enough state that we can start
15131479
# using it.
15141480
self.list_win.border()
15151481
self.list_win.refresh()
1516-
1517-
if self.config.autocomplete_mode == autocomplete.SIMPLE:
1518-
_, self.s = self.matches_iter.cur_line()
1519-
else:
1520-
self.s = self.s[:-len(cw)] + current_match
1521-
1482+
_, self.s = self.matches_iter.cur_line()
15221483
self.print_line(self.s, True)
15231484
return True
15241485

0 commit comments

Comments
 (0)
X Tutup