diff --git a/IPython/core/completer.py b/IPython/core/completer.py index 97fcd0f..155f363 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -61,6 +61,7 @@ used, and this module (and the readline module) are silently inactive. # the file COPYING, distributed as part of this software. # #***************************************************************************** +from __future__ import print_function #----------------------------------------------------------------------------- # Imports @@ -79,7 +80,7 @@ import sys from IPython.core.error import TryNext from IPython.core.prefilter import ESC_MAGIC -from IPython.utils import generics +from IPython.utils import generics, io from IPython.utils.frame import debugx from IPython.utils.dir2 import dir2 @@ -138,9 +139,60 @@ def single_dir_expand(matches): else: return matches -class Bunch: pass -class Completer: +class Bunch(object): pass + + +class CompletionSplitter(object): + """An object to split an input line in a manner similar to readline. + + By having our own implementation, we can expose readline-like completion in + a uniform manner to all frontends. This object only needs to be given the + line of text to be split and the cursor position on said line, and it + returns the 'word' to be completed on at the cursor after splitting the + entire line. + + What characters are used as splitting delimiters can be controlled by + setting the `delims` attribute (this is a property that internally + automatically builds the necessary """ + + # Private interface + + # A string of delimiter characters. The default value makes sense for + # IPython's most typical usage patterns. + _delims = ' \t\n`!@#$^&*()=+[{]}\\|;:\'",<>?' + + # The expression (a normal string) to be compiled into a regular expression + # for actual splitting. We store it as an attribute mostly for ease of + # debugging, since this type of code can be so tricky to debug. + _delim_expr = None + + # The regular expression that does the actual splitting + _delim_re = None + + def __init__(self, delims=None): + delims = CompletionSplitter._delims if delims is None else delims + self.set_delims(delims) + + def set_delims(self, delims): + """Set the delimiters for line splitting.""" + expr = '[' + ''.join('\\'+ c for c in delims) + ']' + self._delim_re = re.compile(expr) + self._delims = delims + self._delim_expr = expr + + def get_delims(self): + """Return the string of delimiter characters.""" + return self._delims + + def split_line(self, line, cursor_pos=None): + """Split a line of text with a cursor at the given position. + """ + l = line if cursor_pos is None else line[:cursor_pos] + return self._delim_re.split(l)[-1] + + +class Completer(object): def __init__(self,namespace=None,global_namespace=None): """Create a new completer for the command line. @@ -631,7 +683,8 @@ class IPCompleter(Completer): Index of the cursor in the full line buffer. Should be provided by remote frontends where kernel has no access to frontend state. """ - + #io.rprint('COMP', text, line_buffer, cursor_pos) # dbg + magic_escape = self.magic_escape self.full_lbuf = line_buffer self.lbuf = self.full_lbuf[:cursor_pos] @@ -663,7 +716,7 @@ class IPCompleter(Completer): # simply collapse the dict into a list for readline, but we'd have # richer completion semantics in other evironments. self.matches = sorted(set(self.matches)) - #from IPython.utils.io import rprint; rprint(self.matches) # dbg + #io.rprint('MATCHES', self.matches) # dbg return self.matches def rlcomplete(self, text, state): @@ -679,15 +732,15 @@ class IPCompleter(Completer): state : int Counter used by readline. - """ - - #print "rlcomplete! '%s' %s" % (text, state) # dbg - if state==0: + self.full_lbuf = line_buffer = self.get_line_buffer() cursor_pos = self.get_endidx() + #io.rprint("\nRLCOMPLETE: %r %r %r" % + # (text, line_buffer, cursor_pos) ) # dbg + # if there is only a tab on a line with only whitespace, instead of # the mostly useless 'do you want to see all million completions' # message, just do the right thing and give the user his tab! @@ -699,7 +752,7 @@ class IPCompleter(Completer): # don't apply this on 'dumb' terminals, such as emacs buffers, so # we don't interfere with their own tab-completion mechanism. - if not (self.dumb_terminal or self.full_lbuf.strip()): + if not (self.dumb_terminal or line_buffer.strip()): self.readline.insert_text('\t') sys.stdout.flush() return None @@ -719,4 +772,3 @@ class IPCompleter(Completer): return self.matches[state] except IndexError: return None - diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index fd453fc..8854b18 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -6,6 +6,7 @@ # stdlib import sys +import unittest # third party import nose.tools as nt @@ -33,3 +34,51 @@ def test_protect_filename(): for s1, s2 in pairs: s1p = completer.protect_filename(s1) nt.assert_equals(s1p, s2) + + +def check_line_split(splitter, test_specs): + for part1, part2, split in test_specs: + cursor_pos = len(part1) + line = part1+part2 + out = splitter.split_line(line, cursor_pos) + nt.assert_equal(out, split) + + +def test_line_split(): + """Basice line splitter test with default specs.""" + sp = completer.CompletionSplitter() + # The format of the test specs is: part1, part2, expected answer. Parts 1 + # and 2 are joined into the 'line' sent to the splitter, as if the cursor + # was at the end of part1. So an empty part2 represents someone hitting + # tab at the end of the line, the most common case. + t = [('run some/scrip', '', 'some/scrip'), + ('run scripts/er', 'ror.py foo', 'scripts/er'), + ('echo $HOM', '', 'HOM'), + ('print sys.pa', '', 'sys.pa'), + ('print(sys.pa', '', 'sys.pa'), + ("execfile('scripts/er", '', 'scripts/er'), + ('a[x.', '', 'x.'), + ('a[x.', 'y', 'x.'), + ('cd "some_file/', '', 'some_file/'), + ] + check_line_split(sp, t) + + +class CompletionSplitterTestCase(unittest.TestCase): + def setUp(self): + self.sp = completer.CompletionSplitter() + + def test_delim_setting(self): + self.sp.delims = ' ' + # Validate that property handling works ok + nt.assert_equal(self.sp.delims, ' ') + nt.assert_equal(self.sp.delim_expr, '[\ ]') + + def test_spaces(self): + """Test with only spaces as split chars.""" + self.sp.delims = ' ' + t = [('foo', '', 'foo'), + ('run foo', '', 'foo'), + ('run foo', 'bar', 'foo'), + ] + check_line_split(self.sp, t)