##// END OF EJS Templates
Do not duplicate '=' when tab completing a kwarg with the cursor on '='
Steve Bartz -
Show More
@@ -1,154 +1,160 b''
1 1 """prompt-toolkit utilities
2 2
3 3 Everything in this module is a private API,
4 4 not to be used outside IPython.
5 5 """
6 6
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 import unicodedata
11 11 from wcwidth import wcwidth
12 12
13 13 from IPython.core.completer import (
14 14 provisionalcompleter, cursor_to_position,
15 15 _deduplicate_completions)
16 16 from prompt_toolkit.completion import Completer, Completion
17 17 from prompt_toolkit.layout.lexers import Lexer
18 18 from prompt_toolkit.layout.lexers import PygmentsLexer
19 19
20 20 import pygments.lexers as pygments_lexers
21 21
22 22 _completion_sentinel = object()
23 23
24 24 def _elide(string, *, min_elide=30):
25 25 """
26 26 If a string is long enough, and has at least 2 dots,
27 27 replace the middle part with ellipses.
28 28
29 29 If three consecutive dots, or two consecutive dots are encountered these are
30 30 replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode
31 31 equivalents
32 32 """
33 33 string = string.replace('...','\N{HORIZONTAL ELLIPSIS}')
34 34 string = string.replace('..','\N{TWO DOT LEADER}')
35 35 if len(string) < min_elide:
36 36 return string
37 37
38 38 parts = string.split('.')
39 39
40 40 if len(parts) <= 3:
41 41 return string
42 42
43 43 return '{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}'.format(parts[0], parts[1][0], parts[-2][-1], parts[-1])
44 44
45 45
46 def _adjust_completion_text_based_on_context(text, body, offset):
47 if text.endswith('=') and len(body) > offset and body[offset] is '=':
48 return text[:-1]
49 else:
50 return text
46 51
47 52
48 53 class IPythonPTCompleter(Completer):
49 54 """Adaptor to provide IPython completions to prompt_toolkit"""
50 55 def __init__(self, ipy_completer=None, shell=None, patch_stdout=None):
51 56 if shell is None and ipy_completer is None:
52 57 raise TypeError("Please pass shell=an InteractiveShell instance.")
53 58 self._ipy_completer = ipy_completer
54 59 self.shell = shell
55 60 if patch_stdout is None:
56 61 raise TypeError("Please pass patch_stdout")
57 62 self.patch_stdout = patch_stdout
58 63
59 64 @property
60 65 def ipy_completer(self):
61 66 if self._ipy_completer:
62 67 return self._ipy_completer
63 68 else:
64 69 return self.shell.Completer
65 70
66 71 def get_completions(self, document, complete_event):
67 72 if not document.current_line.strip():
68 73 return
69 74 # Some bits of our completion system may print stuff (e.g. if a module
70 75 # is imported). This context manager ensures that doesn't interfere with
71 76 # the prompt.
72 77
73 78 with self.patch_stdout(), provisionalcompleter():
74 79 body = document.text
75 80 cursor_row = document.cursor_position_row
76 81 cursor_col = document.cursor_position_col
77 82 cursor_position = document.cursor_position
78 83 offset = cursor_to_position(body, cursor_row, cursor_col)
79 84 yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
80 85
81 86 @staticmethod
82 87 def _get_completions(body, offset, cursor_position, ipyc):
83 88 """
84 89 Private equivalent of get_completions() use only for unit_testing.
85 90 """
86 91 debug = getattr(ipyc, 'debug', False)
87 92 completions = _deduplicate_completions(
88 93 body, ipyc.completions(body, offset))
89 94 for c in completions:
90 95 if not c.text:
91 96 # Guard against completion machinery giving us an empty string.
92 97 continue
93 98 text = unicodedata.normalize('NFC', c.text)
94 99 # When the first character of the completion has a zero length,
95 100 # then it's probably a decomposed unicode character. E.g. caused by
96 101 # the "\dot" completion. Try to compose again with the previous
97 102 # character.
98 103 if wcwidth(text[0]) == 0:
99 104 if cursor_position + c.start > 0:
100 105 char_before = body[c.start - 1]
101 106 fixed_text = unicodedata.normalize(
102 107 'NFC', char_before + text)
103 108
104 109 # Yield the modified completion instead, if this worked.
105 110 if wcwidth(text[0:1]) == 1:
106 111 yield Completion(fixed_text, start_position=c.start - offset - 1)
107 112 continue
108 113
109 114 # TODO: Use Jedi to determine meta_text
110 115 # (Jedi currently has a bug that results in incorrect information.)
111 116 # meta_text = ''
112 117 # yield Completion(m, start_position=start_pos,
113 118 # display_meta=meta_text)
114 119 display_text = c.text
115 120
121 adjusted_text = _adjust_completion_text_based_on_context(c.text, body, offset)
116 122 if c.type == 'function':
117 123 display_text = display_text + '()'
118 124
119 yield Completion(c.text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
125 yield Completion(adjusted_text, start_position=c.start - offset, display=_elide(display_text), display_meta=c.type)
120 126
121 127 class IPythonPTLexer(Lexer):
122 128 """
123 129 Wrapper around PythonLexer and BashLexer.
124 130 """
125 131 def __init__(self):
126 132 l = pygments_lexers
127 133 self.python_lexer = PygmentsLexer(l.Python3Lexer)
128 134 self.shell_lexer = PygmentsLexer(l.BashLexer)
129 135
130 136 self.magic_lexers = {
131 137 'HTML': PygmentsLexer(l.HtmlLexer),
132 138 'html': PygmentsLexer(l.HtmlLexer),
133 139 'javascript': PygmentsLexer(l.JavascriptLexer),
134 140 'js': PygmentsLexer(l.JavascriptLexer),
135 141 'perl': PygmentsLexer(l.PerlLexer),
136 142 'ruby': PygmentsLexer(l.RubyLexer),
137 143 'latex': PygmentsLexer(l.TexLexer),
138 144 }
139 145
140 146 def lex_document(self, cli, document):
141 147 text = document.text.lstrip()
142 148
143 149 lexer = self.python_lexer
144 150
145 151 if text.startswith('!') or text.startswith('%%bash'):
146 152 lexer = self.shell_lexer
147 153
148 154 elif text.startswith('%%'):
149 155 for magic, l in self.magic_lexers.items():
150 156 if text.startswith('%%' + magic):
151 157 lexer = l
152 158 break
153 159
154 160 return lexer.lex_document(cli, document)
@@ -1,163 +1,176 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Tests for the TerminalInteractiveShell and related pieces."""
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import sys
7 7 import unittest
8 8
9 9 from IPython.core.inputtransformer import InputTransformer
10 10 from IPython.testing import tools as tt
11 11 from IPython.utils.capture import capture_output
12 12
13 from IPython.terminal.ptutils import _elide
13 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
14 14 import nose.tools as nt
15 15
16 16 class TestElide(unittest.TestCase):
17 17
18 18 def test_elide(self):
19 19 _elide('concatenate((a1, a2, ...), axis') # do not raise
20 20 _elide('concatenate((a1, a2, ..), . axis') # do not raise
21 21 nt.assert_equal(_elide('aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh'), 'aaaa.b…g.hhhhhh')
22 22
23
24 class TestContextAwareCompletion(unittest.TestCase):
25
26 def test_adjust_completion_text_based_on_context(self):
27 # Adjusted case
28 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a=)', 7), 'arg1')
29
30 # Untouched cases
31 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a)', 7), 'arg1=')
32 nt.assert_equal(_adjust_completion_text_based_on_context('arg1=', 'func1(a', 7), 'arg1=')
33 nt.assert_equal(_adjust_completion_text_based_on_context('%magic', 'func1(a=)', 7), '%magic')
34 nt.assert_equal(_adjust_completion_text_based_on_context('func2', 'func1(a=)', 7), 'func2')
35
23 36 # Decorator for interaction loop tests -----------------------------------------
24 37
25 38 class mock_input_helper(object):
26 39 """Machinery for tests of the main interact loop.
27 40
28 41 Used by the mock_input decorator.
29 42 """
30 43 def __init__(self, testgen):
31 44 self.testgen = testgen
32 45 self.exception = None
33 46 self.ip = get_ipython()
34 47
35 48 def __enter__(self):
36 49 self.orig_prompt_for_code = self.ip.prompt_for_code
37 50 self.ip.prompt_for_code = self.fake_input
38 51 return self
39 52
40 53 def __exit__(self, etype, value, tb):
41 54 self.ip.prompt_for_code = self.orig_prompt_for_code
42 55
43 56 def fake_input(self):
44 57 try:
45 58 return next(self.testgen)
46 59 except StopIteration:
47 60 self.ip.keep_running = False
48 61 return u''
49 62 except:
50 63 self.exception = sys.exc_info()
51 64 self.ip.keep_running = False
52 65 return u''
53 66
54 67 def mock_input(testfunc):
55 68 """Decorator for tests of the main interact loop.
56 69
57 70 Write the test as a generator, yield-ing the input strings, which IPython
58 71 will see as if they were typed in at the prompt.
59 72 """
60 73 def test_method(self):
61 74 testgen = testfunc(self)
62 75 with mock_input_helper(testgen) as mih:
63 76 mih.ip.interact()
64 77
65 78 if mih.exception is not None:
66 79 # Re-raise captured exception
67 80 etype, value, tb = mih.exception
68 81 import traceback
69 82 traceback.print_tb(tb, file=sys.stdout)
70 83 del tb # Avoid reference loop
71 84 raise value
72 85
73 86 return test_method
74 87
75 88 # Test classes -----------------------------------------------------------------
76 89
77 90 class InteractiveShellTestCase(unittest.TestCase):
78 91 def rl_hist_entries(self, rl, n):
79 92 """Get last n readline history entries as a list"""
80 93 return [rl.get_history_item(rl.get_current_history_length() - x)
81 94 for x in range(n - 1, -1, -1)]
82 95
83 96 @mock_input
84 97 def test_inputtransformer_syntaxerror(self):
85 98 ip = get_ipython()
86 99 transformer = SyntaxErrorTransformer()
87 100 ip.input_splitter.python_line_transforms.append(transformer)
88 101 ip.input_transformer_manager.python_line_transforms.append(transformer)
89 102
90 103 try:
91 104 #raise Exception
92 105 with tt.AssertPrints('4', suppress=False):
93 106 yield u'print(2*2)'
94 107
95 108 with tt.AssertPrints('SyntaxError: input contains', suppress=False):
96 109 yield u'print(2345) # syntaxerror'
97 110
98 111 with tt.AssertPrints('16', suppress=False):
99 112 yield u'print(4*4)'
100 113
101 114 finally:
102 115 ip.input_splitter.python_line_transforms.remove(transformer)
103 116 ip.input_transformer_manager.python_line_transforms.remove(transformer)
104 117
105 118 def test_plain_text_only(self):
106 119 ip = get_ipython()
107 120 formatter = ip.display_formatter
108 121 assert formatter.active_types == ['text/plain']
109 122 assert not formatter.ipython_display_formatter.enabled
110 123
111 124 class Test(object):
112 125 def __repr__(self):
113 126 return "<Test %i>" % id(self)
114 127
115 128 def _repr_html_(self):
116 129 return '<html>'
117 130
118 131 # verify that HTML repr isn't computed
119 132 obj = Test()
120 133 data, _ = formatter.format(obj)
121 134 self.assertEqual(data, {'text/plain': repr(obj)})
122 135
123 136 class Test2(Test):
124 137 def _ipython_display_(self):
125 138 from IPython.display import display
126 139 display('<custom>')
127 140
128 141 # verify that _ipython_display_ shortcut isn't called
129 142 obj = Test2()
130 143 with capture_output() as captured:
131 144 data, _ = formatter.format(obj)
132 145
133 146 self.assertEqual(data, {'text/plain': repr(obj)})
134 147 assert captured.stdout == ''
135 148
136 149
137 150
138 151 class SyntaxErrorTransformer(InputTransformer):
139 152 def push(self, line):
140 153 pos = line.find('syntaxerror')
141 154 if pos >= 0:
142 155 e = SyntaxError('input contains "syntaxerror"')
143 156 e.text = line
144 157 e.offset = pos + 1
145 158 raise e
146 159 return line
147 160
148 161 def reset(self):
149 162 pass
150 163
151 164 class TerminalMagicsTestCase(unittest.TestCase):
152 165 def test_paste_magics_blankline(self):
153 166 """Test that code with a blank line doesn't get split (gh-3246)."""
154 167 ip = get_ipython()
155 168 s = ('def pasted_func(a):\n'
156 169 ' b = a+1\n'
157 170 '\n'
158 171 ' return b')
159 172
160 173 tm = ip.magics_manager.registry['TerminalMagics']
161 174 tm.store_or_execute(s, name=None)
162 175
163 176 self.assertEqual(ip.user_ns['pasted_func'](54), 55)
General Comments 0
You need to be logged in to leave comments. Login now