##// END OF EJS Templates
give PTCompleter InteractiveShell, not Completer...
Min RK -
Show More
@@ -1,488 +1,488 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2 from __future__ import print_function
3 3
4 4 import os
5 5 import sys
6 6 import warnings
7 7 from warnings import warn
8 8
9 9 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
10 10 from IPython.utils import io
11 11 from IPython.utils.py3compat import PY3, cast_unicode_py2, input
12 12 from IPython.utils.terminal import toggle_set_term_title, set_term_title
13 13 from IPython.utils.process import abbrev_cwd
14 14 from traitlets import Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum
15 15
16 16 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
17 17 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
18 18 from prompt_toolkit.history import InMemoryHistory
19 19 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout, create_output
20 20 from prompt_toolkit.interface import CommandLineInterface
21 21 from prompt_toolkit.key_binding.manager import KeyBindingManager
22 22 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
23 23 from prompt_toolkit.styles import PygmentsStyle, DynamicStyle
24 24
25 25 from pygments.styles import get_style_by_name, get_all_styles
26 26 from pygments.token import Token
27 27
28 28 from .debugger import TerminalPdb, Pdb
29 29 from .magics import TerminalMagics
30 30 from .pt_inputhooks import get_inputhook_func
31 31 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
32 32 from .ptutils import IPythonPTCompleter, IPythonPTLexer
33 33 from .shortcuts import register_ipython_shortcuts
34 34
35 35 DISPLAY_BANNER_DEPRECATED = object()
36 36
37 37
38 38 from pygments.style import Style
39 39
40 40 class _NoStyle(Style): pass
41 41
42 42
43 43
44 44 _style_overrides_light_bg = {
45 45 Token.Prompt: '#0000ff',
46 46 Token.PromptNum: '#0000ee bold',
47 47 Token.OutPrompt: '#cc0000',
48 48 Token.OutPromptNum: '#bb0000 bold',
49 49 }
50 50
51 51 _style_overrides_linux = {
52 52 Token.Prompt: '#00cc00',
53 53 Token.PromptNum: '#00bb00 bold',
54 54 Token.OutPrompt: '#cc0000',
55 55 Token.OutPromptNum: '#bb0000 bold',
56 56 }
57 57
58 58
59 59
60 60 def get_default_editor():
61 61 try:
62 62 ed = os.environ['EDITOR']
63 63 if not PY3:
64 64 ed = ed.decode()
65 65 return ed
66 66 except KeyError:
67 67 pass
68 68 except UnicodeError:
69 69 warn("$EDITOR environment variable is not pure ASCII. Using platform "
70 70 "default editor.")
71 71
72 72 if os.name == 'posix':
73 73 return 'vi' # the only one guaranteed to be there!
74 74 else:
75 75 return 'notepad' # same in Windows!
76 76
77 77 # conservatively check for tty
78 78 # overridden streams can result in things like:
79 79 # - sys.stdin = None
80 80 # - no isatty method
81 81 for _name in ('stdin', 'stdout', 'stderr'):
82 82 _stream = getattr(sys, _name)
83 83 if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
84 84 _is_tty = False
85 85 break
86 86 else:
87 87 _is_tty = True
88 88
89 89
90 90 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
91 91
92 92 class TerminalInteractiveShell(InteractiveShell):
93 93 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
94 94 'to reserve for the completion menu'
95 95 ).tag(config=True)
96 96
97 97 def _space_for_menu_changed(self, old, new):
98 98 self._update_layout()
99 99
100 100 pt_cli = None
101 101 debugger_history = None
102 102 _pt_app = None
103 103
104 104 simple_prompt = Bool(_use_simple_prompt,
105 105 help="""Use `raw_input` for the REPL, without completion, multiline input, and prompt colors.
106 106
107 107 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
108 108 IPython own testing machinery, and emacs inferior-shell integration through elpy.
109 109
110 110 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
111 111 environment variable is set, or the current terminal is not a tty.
112 112
113 113 """
114 114 ).tag(config=True)
115 115
116 116 @property
117 117 def debugger_cls(self):
118 118 return Pdb if self.simple_prompt else TerminalPdb
119 119
120 120 confirm_exit = Bool(True,
121 121 help="""
122 122 Set to confirm when you try to exit IPython with an EOF (Control-D
123 123 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
124 124 you can force a direct exit without any confirmation.""",
125 125 ).tag(config=True)
126 126
127 127 editing_mode = Unicode('emacs',
128 128 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
129 129 ).tag(config=True)
130 130
131 131 mouse_support = Bool(False,
132 132 help="Enable mouse support in the prompt"
133 133 ).tag(config=True)
134 134
135 135 highlighting_style = Unicode('legacy',
136 136 help="The name of a Pygments style to use for syntax highlighting: \n %s" % ', '.join(get_all_styles())
137 137 ).tag(config=True)
138 138
139 139
140 140 @observe('highlighting_style')
141 141 @observe('colors')
142 142 def _highlighting_style_changed(self, change):
143 143 self.refresh_style()
144 144
145 145 def refresh_style(self):
146 146 self._style = self._make_style_from_name(self.highlighting_style)
147 147
148 148
149 149 highlighting_style_overrides = Dict(
150 150 help="Override highlighting format for specific tokens"
151 151 ).tag(config=True)
152 152
153 153 true_color = Bool(False,
154 154 help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
155 155 "If your terminal supports true color, the following command "
156 156 "should print 'TRUECOLOR' in orange: "
157 157 "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
158 158 ).tag(config=True)
159 159
160 160 editor = Unicode(get_default_editor(),
161 161 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
162 162 ).tag(config=True)
163 163
164 164 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
165 165
166 166 prompts = Instance(Prompts)
167 167
168 168 @default('prompts')
169 169 def _prompts_default(self):
170 170 return self.prompts_class(self)
171 171
172 172 @observe('prompts')
173 173 def _(self, change):
174 174 self._update_layout()
175 175
176 176 @default('displayhook_class')
177 177 def _displayhook_class_default(self):
178 178 return RichPromptDisplayHook
179 179
180 180 term_title = Bool(True,
181 181 help="Automatically set the terminal title"
182 182 ).tag(config=True)
183 183
184 184 display_completions = Enum(('column', 'multicolumn','readlinelike'),
185 185 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
186 186 "'readlinelike'. These options are for `prompt_toolkit`, see "
187 187 "`prompt_toolkit` documentation for more information."
188 188 ),
189 189 default_value='multicolumn').tag(config=True)
190 190
191 191 highlight_matching_brackets = Bool(True,
192 192 help="Highlight matching brackets .",
193 193 ).tag(config=True)
194 194
195 195 @observe('term_title')
196 196 def init_term_title(self, change=None):
197 197 # Enable or disable the terminal title.
198 198 if self.term_title:
199 199 toggle_set_term_title(True)
200 200 set_term_title('IPython: ' + abbrev_cwd())
201 201 else:
202 202 toggle_set_term_title(False)
203 203
204 204 def init_display_formatter(self):
205 205 super(TerminalInteractiveShell, self).init_display_formatter()
206 206 # terminal only supports plain text
207 207 self.display_formatter.active_types = ['text/plain']
208 208
209 209 def init_prompt_toolkit_cli(self):
210 210 if self.simple_prompt:
211 211 # Fall back to plain non-interactive output for tests.
212 212 # This is very limited, and only accepts a single line.
213 213 def prompt():
214 214 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
215 215 self.prompt_for_code = prompt
216 216 return
217 217
218 218 # Set up keyboard shortcuts
219 219 kbmanager = KeyBindingManager.for_prompt()
220 220 register_ipython_shortcuts(kbmanager.registry, self)
221 221
222 222 # Pre-populate history from IPython's history database
223 223 history = InMemoryHistory()
224 224 last_cell = u""
225 225 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
226 226 include_latest=True):
227 227 # Ignore blank lines and consecutive duplicates
228 228 cell = cell.rstrip()
229 229 if cell and (cell != last_cell):
230 230 history.append(cell)
231 231
232 232 self._style = self._make_style_from_name(self.highlighting_style)
233 233 style = DynamicStyle(lambda: self._style)
234 234
235 235 editing_mode = getattr(EditingMode, self.editing_mode.upper())
236 236
237 237 self._pt_app = create_prompt_application(
238 238 editing_mode=editing_mode,
239 239 key_bindings_registry=kbmanager.registry,
240 240 history=history,
241 completer=IPythonPTCompleter(self.Completer),
241 completer=IPythonPTCompleter(self),
242 242 enable_history_search=True,
243 243 style=style,
244 244 mouse_support=self.mouse_support,
245 245 **self._layout_options()
246 246 )
247 247 self._eventloop = create_eventloop(self.inputhook)
248 248 self.pt_cli = CommandLineInterface(
249 249 self._pt_app, eventloop=self._eventloop,
250 250 output=create_output(true_color=self.true_color))
251 251
252 252 def _make_style_from_name(self, name):
253 253 """
254 254 Small wrapper that make an IPython compatible style from a style name
255 255
256 256 We need that to add style for prompt ... etc.
257 257 """
258 258 style_overrides = {}
259 259 if name == 'legacy':
260 260 legacy = self.colors.lower()
261 261 if legacy == 'linux':
262 262 style_cls = get_style_by_name('monokai')
263 263 style_overrides = _style_overrides_linux
264 264 elif legacy == 'lightbg':
265 265 style_overrides = _style_overrides_light_bg
266 266 style_cls = get_style_by_name('pastie')
267 267 elif legacy == 'neutral':
268 268 # The default theme needs to be visible on both a dark background
269 269 # and a light background, because we can't tell what the terminal
270 270 # looks like. These tweaks to the default theme help with that.
271 271 style_cls = get_style_by_name('default')
272 272 style_overrides.update({
273 273 Token.Number: '#007700',
274 274 Token.Operator: 'noinherit',
275 275 Token.String: '#BB6622',
276 276 Token.Name.Function: '#2080D0',
277 277 Token.Name.Class: 'bold #2080D0',
278 278 Token.Name.Namespace: 'bold #2080D0',
279 279 Token.Prompt: '#009900',
280 280 Token.PromptNum: '#00ff00 bold',
281 281 Token.OutPrompt: '#990000',
282 282 Token.OutPromptNum: '#ff0000 bold',
283 283 })
284 284 elif legacy =='nocolor':
285 285 style_cls=_NoStyle
286 286 style_overrides = {}
287 287 else :
288 288 raise ValueError('Got unknown colors: ', legacy)
289 289 else :
290 290 style_cls = get_style_by_name(name)
291 291 style_overrides = {
292 292 Token.Prompt: '#009900',
293 293 Token.PromptNum: '#00ff00 bold',
294 294 Token.OutPrompt: '#990000',
295 295 Token.OutPromptNum: '#ff0000 bold',
296 296 }
297 297 style_overrides.update(self.highlighting_style_overrides)
298 298 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
299 299 style_dict=style_overrides)
300 300
301 301 return style
302 302
303 303 def _layout_options(self):
304 304 """
305 305 Return the current layout option for the current Terminal InteractiveShell
306 306 """
307 307 return {
308 308 'lexer':IPythonPTLexer(),
309 309 'reserve_space_for_menu':self.space_for_menu,
310 310 'get_prompt_tokens':self.prompts.in_prompt_tokens,
311 311 'get_continuation_tokens':self.prompts.continuation_prompt_tokens,
312 312 'multiline':True,
313 313 'display_completions_in_columns': (self.display_completions == 'multicolumn'),
314 314
315 315 # Highlight matching brackets, but only when this setting is
316 316 # enabled, and only when the DEFAULT_BUFFER has the focus.
317 317 'extra_input_processors': [ConditionalProcessor(
318 318 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
319 319 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
320 320 Condition(lambda cli: self.highlight_matching_brackets))],
321 321 }
322 322
323 323 def _update_layout(self):
324 324 """
325 325 Ask for a re computation of the application layout, if for example ,
326 326 some configuration options have changed.
327 327 """
328 328 if self._pt_app:
329 329 self._pt_app.layout = create_prompt_layout(**self._layout_options())
330 330
331 331 def prompt_for_code(self):
332 332 document = self.pt_cli.run(
333 333 pre_run=self.pre_prompt, reset_current_buffer=True)
334 334 return document.text
335 335
336 336 def enable_win_unicode_console(self):
337 337 import win_unicode_console
338 338
339 339 if PY3:
340 340 win_unicode_console.enable()
341 341 else:
342 342 # https://github.com/ipython/ipython/issues/9768
343 343 from win_unicode_console.streams import (TextStreamWrapper,
344 344 stdout_text_transcoded, stderr_text_transcoded)
345 345
346 346 class LenientStrStreamWrapper(TextStreamWrapper):
347 347 def write(self, s):
348 348 if isinstance(s, bytes):
349 349 s = s.decode(self.encoding, 'replace')
350 350
351 351 self.base.write(s)
352 352
353 353 stdout_text_str = LenientStrStreamWrapper(stdout_text_transcoded)
354 354 stderr_text_str = LenientStrStreamWrapper(stderr_text_transcoded)
355 355
356 356 win_unicode_console.enable(stdout=stdout_text_str,
357 357 stderr=stderr_text_str)
358 358
359 359 def init_io(self):
360 360 if sys.platform not in {'win32', 'cli'}:
361 361 return
362 362
363 363 self.enable_win_unicode_console()
364 364
365 365 import colorama
366 366 colorama.init()
367 367
368 368 # For some reason we make these wrappers around stdout/stderr.
369 369 # For now, we need to reset them so all output gets coloured.
370 370 # https://github.com/ipython/ipython/issues/8669
371 371 # io.std* are deprecated, but don't show our own deprecation warnings
372 372 # during initialization of the deprecated API.
373 373 with warnings.catch_warnings():
374 374 warnings.simplefilter('ignore', DeprecationWarning)
375 375 io.stdout = io.IOStream(sys.stdout)
376 376 io.stderr = io.IOStream(sys.stderr)
377 377
378 378 def init_magics(self):
379 379 super(TerminalInteractiveShell, self).init_magics()
380 380 self.register_magics(TerminalMagics)
381 381
382 382 def init_alias(self):
383 383 # The parent class defines aliases that can be safely used with any
384 384 # frontend.
385 385 super(TerminalInteractiveShell, self).init_alias()
386 386
387 387 # Now define aliases that only make sense on the terminal, because they
388 388 # need direct access to the console in a way that we can't emulate in
389 389 # GUI or web frontend
390 390 if os.name == 'posix':
391 391 for cmd in ['clear', 'more', 'less', 'man']:
392 392 self.alias_manager.soft_define_alias(cmd, cmd)
393 393
394 394
395 395 def __init__(self, *args, **kwargs):
396 396 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
397 397 self.init_prompt_toolkit_cli()
398 398 self.init_term_title()
399 399 self.keep_running = True
400 400
401 401 self.debugger_history = InMemoryHistory()
402 402
403 403 def ask_exit(self):
404 404 self.keep_running = False
405 405
406 406 rl_next_input = None
407 407
408 408 def pre_prompt(self):
409 409 if self.rl_next_input:
410 410 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
411 411 self.rl_next_input = None
412 412
413 413 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
414 414
415 415 if display_banner is not DISPLAY_BANNER_DEPRECATED:
416 416 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
417 417
418 418 self.keep_running = True
419 419 while self.keep_running:
420 420 print(self.separate_in, end='')
421 421
422 422 try:
423 423 code = self.prompt_for_code()
424 424 except EOFError:
425 425 if (not self.confirm_exit) \
426 426 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
427 427 self.ask_exit()
428 428
429 429 else:
430 430 if code:
431 431 self.run_cell(code, store_history=True)
432 432
433 433 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
434 434 # An extra layer of protection in case someone mashing Ctrl-C breaks
435 435 # out of our internal code.
436 436 if display_banner is not DISPLAY_BANNER_DEPRECATED:
437 437 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
438 438 while True:
439 439 try:
440 440 self.interact()
441 441 break
442 442 except KeyboardInterrupt:
443 443 print("\nKeyboardInterrupt escaped interact()\n")
444 444
445 445 _inputhook = None
446 446 def inputhook(self, context):
447 447 if self._inputhook is not None:
448 448 self._inputhook(context)
449 449
450 450 def enable_gui(self, gui=None):
451 451 if gui:
452 452 self._inputhook = get_inputhook_func(gui)
453 453 else:
454 454 self._inputhook = None
455 455
456 456 # Run !system commands directly, not through pipes, so terminal programs
457 457 # work correctly.
458 458 system = InteractiveShell.system_raw
459 459
460 460 def auto_rewrite_input(self, cmd):
461 461 """Overridden from the parent class to use fancy rewriting prompt"""
462 462 if not self.show_rewritten_input:
463 463 return
464 464
465 465 tokens = self.prompts.rewrite_prompt_tokens()
466 466 if self.pt_cli:
467 467 self.pt_cli.print_tokens(tokens)
468 468 print(cmd)
469 469 else:
470 470 prompt = ''.join(s for t, s in tokens)
471 471 print(prompt, cmd, sep='')
472 472
473 473 _prompts_before = None
474 474 def switch_doctest_mode(self, mode):
475 475 """Switch prompts to classic for %doctest_mode"""
476 476 if mode:
477 477 self._prompts_before = self.prompts
478 478 self.prompts = ClassicPrompts(self)
479 479 elif self._prompts_before:
480 480 self.prompts = self._prompts_before
481 481 self._prompts_before = None
482 482 self._update_layout()
483 483
484 484
485 485 InteractiveShellABC.register(TerminalInteractiveShell)
486 486
487 487 if __name__ == '__main__':
488 488 TerminalInteractiveShell.instance().interact()
@@ -1,88 +1,92 b''
1 1 import unicodedata
2 2 from wcwidth import wcwidth
3 3
4 4 from IPython.utils.py3compat import PY3
5 5
6 6 from prompt_toolkit.completion import Completer, Completion
7 7 from prompt_toolkit.layout.lexers import Lexer
8 8 from prompt_toolkit.layout.lexers import PygmentsLexer
9 9
10 10 import pygments.lexers as pygments_lexers
11 11
12 12
13 13 class IPythonPTCompleter(Completer):
14 14 """Adaptor to provide IPython completions to prompt_toolkit"""
15 def __init__(self, ipy_completer):
16 self.ipy_completer = ipy_completer
15 def __init__(self, shell):
16 self.shell = shell
17
18 @property
19 def ipy_completer(self):
20 return self.shell.Completer
17 21
18 22 def get_completions(self, document, complete_event):
19 23 if not document.current_line.strip():
20 24 return
21 25
22 26 used, matches = self.ipy_completer.complete(
23 27 line_buffer=document.current_line,
24 28 cursor_pos=document.cursor_position_col
25 29 )
26 30 start_pos = -len(used)
27 31 for m in matches:
28 32 if not m:
29 33 # Guard against completion machinery giving us an empty string.
30 34 continue
31 35
32 36 m = unicodedata.normalize('NFC', m)
33 37
34 38 # When the first character of the completion has a zero length,
35 39 # then it's probably a decomposed unicode character. E.g. caused by
36 40 # the "\dot" completion. Try to compose again with the previous
37 41 # character.
38 42 if wcwidth(m[0]) == 0:
39 43 if document.cursor_position + start_pos > 0:
40 44 char_before = document.text[document.cursor_position + start_pos - 1]
41 45 m = unicodedata.normalize('NFC', char_before + m)
42 46
43 47 # Yield the modified completion instead, if this worked.
44 48 if wcwidth(m[0:1]) == 1:
45 49 yield Completion(m, start_position=start_pos - 1)
46 50 continue
47 51
48 52 # TODO: Use Jedi to determine meta_text
49 53 # (Jedi currently has a bug that results in incorrect information.)
50 54 # meta_text = ''
51 55 # yield Completion(m, start_position=start_pos,
52 56 # display_meta=meta_text)
53 57 yield Completion(m, start_position=start_pos)
54 58
55 59 class IPythonPTLexer(Lexer):
56 60 """
57 61 Wrapper around PythonLexer and BashLexer.
58 62 """
59 63 def __init__(self):
60 64 l = pygments_lexers
61 65 self.python_lexer = PygmentsLexer(l.Python3Lexer if PY3 else l.PythonLexer)
62 66 self.shell_lexer = PygmentsLexer(l.BashLexer)
63 67
64 68 self.magic_lexers = {
65 69 'HTML': PygmentsLexer(l.HtmlLexer),
66 70 'html': PygmentsLexer(l.HtmlLexer),
67 71 'javascript': PygmentsLexer(l.JavascriptLexer),
68 72 'js': PygmentsLexer(l.JavascriptLexer),
69 73 'perl': PygmentsLexer(l.PerlLexer),
70 74 'ruby': PygmentsLexer(l.RubyLexer),
71 75 'latex': PygmentsLexer(l.TexLexer),
72 76 }
73 77
74 78 def lex_document(self, cli, document):
75 79 text = document.text.lstrip()
76 80
77 81 lexer = self.python_lexer
78 82
79 83 if text.startswith('!') or text.startswith('%%bash'):
80 84 lexer = self.shell_lexer
81 85
82 86 elif text.startswith('%%'):
83 87 for magic, l in self.magic_lexers.items():
84 88 if text.startswith('%%' + magic):
85 89 lexer = l
86 90 break
87 91
88 92 return lexer.lex_document(cli, document)
General Comments 0
You need to be logged in to leave comments. Login now