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