##// END OF EJS Templates
making prompt_tolkit history search configurable
Shailyn javier Ortiz jimenez -
Show More
@@ -1,540 +1,542 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2
3 3 import os
4 4 import sys
5 5 import warnings
6 6 from warnings import warn
7 7
8 8 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
9 9 from IPython.utils import io
10 10 from IPython.utils.py3compat import input
11 11 from IPython.utils.terminal import toggle_set_term_title, set_term_title
12 12 from IPython.utils.process import abbrev_cwd
13 13 from traitlets import (
14 14 Bool, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union,
15 15 Any,
16 16 )
17 17
18 18 from prompt_toolkit.document import Document
19 19 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
20 20 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
21 21 from prompt_toolkit.history import InMemoryHistory
22 22 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout, create_output
23 23 from prompt_toolkit.interface import CommandLineInterface
24 24 from prompt_toolkit.key_binding.manager import KeyBindingManager
25 25 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
26 26 from prompt_toolkit.styles import PygmentsStyle, DynamicStyle
27 27
28 28 from pygments.styles import get_style_by_name
29 29 from pygments.style import Style
30 30 from pygments.token import Token
31 31
32 32 from .debugger import TerminalPdb, Pdb
33 33 from .magics import TerminalMagics
34 34 from .pt_inputhooks import get_inputhook_name_and_func
35 35 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
36 36 from .ptutils import IPythonPTCompleter, IPythonPTLexer
37 37 from .shortcuts import register_ipython_shortcuts
38 38
39 39 DISPLAY_BANNER_DEPRECATED = object()
40 40
41 41
42 42 class _NoStyle(Style): pass
43 43
44 44
45 45
46 46 _style_overrides_light_bg = {
47 47 Token.Prompt: '#0000ff',
48 48 Token.PromptNum: '#0000ee bold',
49 49 Token.OutPrompt: '#cc0000',
50 50 Token.OutPromptNum: '#bb0000 bold',
51 51 }
52 52
53 53 _style_overrides_linux = {
54 54 Token.Prompt: '#00cc00',
55 55 Token.PromptNum: '#00bb00 bold',
56 56 Token.OutPrompt: '#cc0000',
57 57 Token.OutPromptNum: '#bb0000 bold',
58 58 }
59 59
60
61
62 60 def get_default_editor():
63 61 try:
64 62 return os.environ['EDITOR']
65 63 except KeyError:
66 64 pass
67 65 except UnicodeError:
68 66 warn("$EDITOR environment variable is not pure ASCII. Using platform "
69 67 "default editor.")
70 68
71 69 if os.name == 'posix':
72 70 return 'vi' # the only one guaranteed to be there!
73 71 else:
74 72 return 'notepad' # same in Windows!
75 73
76 74 # conservatively check for tty
77 75 # overridden streams can result in things like:
78 76 # - sys.stdin = None
79 77 # - no isatty method
80 78 for _name in ('stdin', 'stdout', 'stderr'):
81 79 _stream = getattr(sys, _name)
82 80 if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
83 81 _is_tty = False
84 82 break
85 83 else:
86 84 _is_tty = True
87 85
88 86
89 87 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
90 88
91 89 class TerminalInteractiveShell(InteractiveShell):
92 90 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
93 91 'to reserve for the completion menu'
94 92 ).tag(config=True)
95 93
96 94 def _space_for_menu_changed(self, old, new):
97 95 self._update_layout()
98 96
99 97 pt_cli = None
100 98 debugger_history = None
101 99 _pt_app = None
102 100
103 101 simple_prompt = Bool(_use_simple_prompt,
104 102 help="""Use `raw_input` for the REPL, without completion and prompt colors.
105 103
106 104 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
107 105 IPython own testing machinery, and emacs inferior-shell integration through elpy.
108 106
109 107 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
110 108 environment variable is set, or the current terminal is not a tty."""
111 109 ).tag(config=True)
112 110
113 111 @property
114 112 def debugger_cls(self):
115 113 return Pdb if self.simple_prompt else TerminalPdb
116 114
117 115 confirm_exit = Bool(True,
118 116 help="""
119 117 Set to confirm when you try to exit IPython with an EOF (Control-D
120 118 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
121 119 you can force a direct exit without any confirmation.""",
122 120 ).tag(config=True)
123 121
124 122 editing_mode = Unicode('emacs',
125 123 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
126 124 ).tag(config=True)
127 125
128 126 mouse_support = Bool(False,
129 127 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
130 128 ).tag(config=True)
131 129
132 130 # We don't load the list of styles for the help string, because loading
133 131 # Pygments plugins takes time and can cause unexpected errors.
134 132 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
135 133 help="""The name or class of a Pygments style to use for syntax
136 134 highlighting. To see available styles, run `pygmentize -L styles`."""
137 135 ).tag(config=True)
138 136
139 137
140 138 @observe('highlighting_style')
141 139 @observe('colors')
142 140 def _highlighting_style_changed(self, change):
143 141 self.refresh_style()
144 142
145 143 def refresh_style(self):
146 144 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
147 145
148 146
149 147 highlighting_style_overrides = Dict(
150 148 help="Override highlighting format for specific tokens"
151 149 ).tag(config=True)
152 150
153 151 true_color = Bool(False,
154 152 help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
155 153 "If your terminal supports true color, the following command "
156 154 "should print 'TRUECOLOR' in orange: "
157 155 "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
158 156 ).tag(config=True)
159 157
160 158 editor = Unicode(get_default_editor(),
161 159 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
162 160 ).tag(config=True)
163 161
164 162 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
165 163
166 164 prompts = Instance(Prompts)
167 165
168 166 @default('prompts')
169 167 def _prompts_default(self):
170 168 return self.prompts_class(self)
171 169
172 170 @observe('prompts')
173 171 def _(self, change):
174 172 self._update_layout()
175 173
176 174 @default('displayhook_class')
177 175 def _displayhook_class_default(self):
178 176 return RichPromptDisplayHook
179 177
180 178 term_title = Bool(True,
181 179 help="Automatically set the terminal title"
182 180 ).tag(config=True)
183 181
184 182 term_title_format = Unicode("IPython: {cwd}",
185 183 help="Customize the terminal title format. This is a python format string. " +
186 184 "Available substitutions are: {cwd}."
187 185 ).tag(config=True)
188 186
189 187 display_completions = Enum(('column', 'multicolumn','readlinelike'),
190 188 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
191 189 "'readlinelike'. These options are for `prompt_toolkit`, see "
192 190 "`prompt_toolkit` documentation for more information."
193 191 ),
194 192 default_value='multicolumn').tag(config=True)
195 193
196 194 highlight_matching_brackets = Bool(True,
197 195 help="Highlight matching brackets.",
198 196 ).tag(config=True)
199 197
200 198 extra_open_editor_shortcuts = Bool(False,
201 199 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
202 200 "This is in addition to the F2 binding, which is always enabled."
203 201 ).tag(config=True)
204 202
205 203 handle_return = Any(None,
206 204 help="Provide an alternative handler to be called when the user presses "
207 205 "Return. This is an advanced option intended for debugging, which "
208 206 "may be changed or removed in later releases."
209 207 ).tag(config=True)
210 208
211 209 @observe('term_title')
212 210 def init_term_title(self, change=None):
213 211 # Enable or disable the terminal title.
214 212 if self.term_title:
215 213 toggle_set_term_title(True)
216 214 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
217 215 else:
218 216 toggle_set_term_title(False)
219 217
220 218 def init_display_formatter(self):
221 219 super(TerminalInteractiveShell, self).init_display_formatter()
222 220 # terminal only supports plain text
223 221 self.display_formatter.active_types = ['text/plain']
224 222 # disable `_ipython_display_`
225 223 self.display_formatter.ipython_display_formatter.enabled = False
226 224
227 225 def init_prompt_toolkit_cli(self):
228 226 if self.simple_prompt:
229 227 # Fall back to plain non-interactive output for tests.
230 228 # This is very limited, and only accepts a single line.
231 229 def prompt():
232 230 isp = self.input_splitter
233 231 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
234 232 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
235 233 while isp.push_accepts_more():
236 234 line = input(prompt_text)
237 235 isp.push(line)
238 236 prompt_text = prompt_continuation
239 237 return isp.source_reset()
240 238 self.prompt_for_code = prompt
241 239 return
242 240
243 241 # Set up keyboard shortcuts
244 242 kbmanager = KeyBindingManager.for_prompt(
245 243 enable_open_in_editor=self.extra_open_editor_shortcuts,
246 244 )
247 245 register_ipython_shortcuts(kbmanager.registry, self)
248 246
249 247 # Pre-populate history from IPython's history database
250 248 history = InMemoryHistory()
251 249 last_cell = u""
252 250 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
253 251 include_latest=True):
254 252 # Ignore blank lines and consecutive duplicates
255 253 cell = cell.rstrip()
256 254 if cell and (cell != last_cell):
257 255 history.append(cell)
258 256 last_cell = cell
259 257
260 258 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
261 259 self.style = DynamicStyle(lambda: self._style)
262 260
263 261 editing_mode = getattr(EditingMode, self.editing_mode.upper())
264 262
265 263 def patch_stdout(**kwargs):
266 264 return self.pt_cli.patch_stdout_context(**kwargs)
267 265
268 266 self._pt_app = create_prompt_application(
269 267 editing_mode=editing_mode,
270 268 key_bindings_registry=kbmanager.registry,
271 269 history=history,
272 270 completer=IPythonPTCompleter(shell=self,
273 271 patch_stdout=patch_stdout),
274 enable_history_search=True,
272 enable_history_search=(
273 os.environ['enable_history_search'].capitalize()
274 if 'enable_history_search' in os.environ
275 else True
276 ),
275 277 style=self.style,
276 278 mouse_support=self.mouse_support,
277 279 **self._layout_options()
278 280 )
279 281 self._eventloop = create_eventloop(self.inputhook)
280 282 self.pt_cli = CommandLineInterface(
281 283 self._pt_app, eventloop=self._eventloop,
282 284 output=create_output(true_color=self.true_color))
283 285
284 286 def _make_style_from_name_or_cls(self, name_or_cls):
285 287 """
286 288 Small wrapper that make an IPython compatible style from a style name
287 289
288 290 We need that to add style for prompt ... etc.
289 291 """
290 292 style_overrides = {}
291 293 if name_or_cls == 'legacy':
292 294 legacy = self.colors.lower()
293 295 if legacy == 'linux':
294 296 style_cls = get_style_by_name('monokai')
295 297 style_overrides = _style_overrides_linux
296 298 elif legacy == 'lightbg':
297 299 style_overrides = _style_overrides_light_bg
298 300 style_cls = get_style_by_name('pastie')
299 301 elif legacy == 'neutral':
300 302 # The default theme needs to be visible on both a dark background
301 303 # and a light background, because we can't tell what the terminal
302 304 # looks like. These tweaks to the default theme help with that.
303 305 style_cls = get_style_by_name('default')
304 306 style_overrides.update({
305 307 Token.Number: '#007700',
306 308 Token.Operator: 'noinherit',
307 309 Token.String: '#BB6622',
308 310 Token.Name.Function: '#2080D0',
309 311 Token.Name.Class: 'bold #2080D0',
310 312 Token.Name.Namespace: 'bold #2080D0',
311 313 Token.Prompt: '#009900',
312 314 Token.PromptNum: '#00ff00 bold',
313 315 Token.OutPrompt: '#990000',
314 316 Token.OutPromptNum: '#ff0000 bold',
315 317 })
316 318
317 319 # Hack: Due to limited color support on the Windows console
318 320 # the prompt colors will be wrong without this
319 321 if os.name == 'nt':
320 322 style_overrides.update({
321 323 Token.Prompt: '#ansidarkgreen',
322 324 Token.PromptNum: '#ansigreen bold',
323 325 Token.OutPrompt: '#ansidarkred',
324 326 Token.OutPromptNum: '#ansired bold',
325 327 })
326 328 elif legacy =='nocolor':
327 329 style_cls=_NoStyle
328 330 style_overrides = {}
329 331 else :
330 332 raise ValueError('Got unknown colors: ', legacy)
331 333 else :
332 334 if isinstance(name_or_cls, str):
333 335 style_cls = get_style_by_name(name_or_cls)
334 336 else:
335 337 style_cls = name_or_cls
336 338 style_overrides = {
337 339 Token.Prompt: '#009900',
338 340 Token.PromptNum: '#00ff00 bold',
339 341 Token.OutPrompt: '#990000',
340 342 Token.OutPromptNum: '#ff0000 bold',
341 343 }
342 344 style_overrides.update(self.highlighting_style_overrides)
343 345 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
344 346 style_dict=style_overrides)
345 347
346 348 return style
347 349
348 350 def _layout_options(self):
349 351 """
350 352 Return the current layout option for the current Terminal InteractiveShell
351 353 """
352 354 return {
353 355 'lexer':IPythonPTLexer(),
354 356 'reserve_space_for_menu':self.space_for_menu,
355 357 'get_prompt_tokens':self.prompts.in_prompt_tokens,
356 358 'get_continuation_tokens':self.prompts.continuation_prompt_tokens,
357 359 'multiline':True,
358 360 'display_completions_in_columns': (self.display_completions == 'multicolumn'),
359 361
360 362 # Highlight matching brackets, but only when this setting is
361 363 # enabled, and only when the DEFAULT_BUFFER has the focus.
362 364 'extra_input_processors': [ConditionalProcessor(
363 365 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
364 366 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
365 367 Condition(lambda cli: self.highlight_matching_brackets))],
366 368 }
367 369
368 370 def _update_layout(self):
369 371 """
370 372 Ask for a re computation of the application layout, if for example ,
371 373 some configuration options have changed.
372 374 """
373 375 if self._pt_app:
374 376 self._pt_app.layout = create_prompt_layout(**self._layout_options())
375 377
376 378 def prompt_for_code(self):
377 379 with self.pt_cli.patch_stdout_context(raw=True):
378 380 document = self.pt_cli.run(
379 381 pre_run=self.pre_prompt, reset_current_buffer=True)
380 382 return document.text
381 383
382 384 def enable_win_unicode_console(self):
383 385 if sys.version_info >= (3, 6):
384 386 # Since PEP 528, Python uses the unicode APIs for the Windows
385 387 # console by default, so WUC shouldn't be needed.
386 388 return
387 389
388 390 import win_unicode_console
389 391 win_unicode_console.enable()
390 392
391 393 def init_io(self):
392 394 if sys.platform not in {'win32', 'cli'}:
393 395 return
394 396
395 397 self.enable_win_unicode_console()
396 398
397 399 import colorama
398 400 colorama.init()
399 401
400 402 # For some reason we make these wrappers around stdout/stderr.
401 403 # For now, we need to reset them so all output gets coloured.
402 404 # https://github.com/ipython/ipython/issues/8669
403 405 # io.std* are deprecated, but don't show our own deprecation warnings
404 406 # during initialization of the deprecated API.
405 407 with warnings.catch_warnings():
406 408 warnings.simplefilter('ignore', DeprecationWarning)
407 409 io.stdout = io.IOStream(sys.stdout)
408 410 io.stderr = io.IOStream(sys.stderr)
409 411
410 412 def init_magics(self):
411 413 super(TerminalInteractiveShell, self).init_magics()
412 414 self.register_magics(TerminalMagics)
413 415
414 416 def init_alias(self):
415 417 # The parent class defines aliases that can be safely used with any
416 418 # frontend.
417 419 super(TerminalInteractiveShell, self).init_alias()
418 420
419 421 # Now define aliases that only make sense on the terminal, because they
420 422 # need direct access to the console in a way that we can't emulate in
421 423 # GUI or web frontend
422 424 if os.name == 'posix':
423 425 for cmd in ['clear', 'more', 'less', 'man']:
424 426 self.alias_manager.soft_define_alias(cmd, cmd)
425 427
426 428
427 429 def __init__(self, *args, **kwargs):
428 430 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
429 431 self.init_prompt_toolkit_cli()
430 432 self.init_term_title()
431 433 self.keep_running = True
432 434
433 435 self.debugger_history = InMemoryHistory()
434 436
435 437 def ask_exit(self):
436 438 self.keep_running = False
437 439
438 440 rl_next_input = None
439 441
440 442 def pre_prompt(self):
441 443 if self.rl_next_input:
442 444 # We can't set the buffer here, because it will be reset just after
443 445 # this. Adding a callable to pre_run_callables does what we need
444 446 # after the buffer is reset.
445 447 s = self.rl_next_input
446 448 def set_doc():
447 449 self.pt_cli.application.buffer.document = Document(s)
448 450 if hasattr(self.pt_cli, 'pre_run_callables'):
449 451 self.pt_cli.pre_run_callables.append(set_doc)
450 452 else:
451 453 # Older version of prompt_toolkit; it's OK to set the document
452 454 # directly here.
453 455 set_doc()
454 456 self.rl_next_input = None
455 457
456 458 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
457 459
458 460 if display_banner is not DISPLAY_BANNER_DEPRECATED:
459 461 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
460 462
461 463 self.keep_running = True
462 464 while self.keep_running:
463 465 print(self.separate_in, end='')
464 466
465 467 try:
466 468 code = self.prompt_for_code()
467 469 except EOFError:
468 470 if (not self.confirm_exit) \
469 471 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
470 472 self.ask_exit()
471 473
472 474 else:
473 475 if code:
474 476 self.run_cell(code, store_history=True)
475 477
476 478 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
477 479 # An extra layer of protection in case someone mashing Ctrl-C breaks
478 480 # out of our internal code.
479 481 if display_banner is not DISPLAY_BANNER_DEPRECATED:
480 482 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
481 483 while True:
482 484 try:
483 485 self.interact()
484 486 break
485 487 except KeyboardInterrupt as e:
486 488 print("\n%s escaped interact()\n" % type(e).__name__)
487 489 finally:
488 490 # An interrupt during the eventloop will mess up the
489 491 # internal state of the prompt_toolkit library.
490 492 # Stopping the eventloop fixes this, see
491 493 # https://github.com/ipython/ipython/pull/9867
492 494 if hasattr(self, '_eventloop'):
493 495 self._eventloop.stop()
494 496
495 497 _inputhook = None
496 498 def inputhook(self, context):
497 499 if self._inputhook is not None:
498 500 self._inputhook(context)
499 501
500 502 active_eventloop = None
501 503 def enable_gui(self, gui=None):
502 504 if gui:
503 505 self.active_eventloop, self._inputhook =\
504 506 get_inputhook_name_and_func(gui)
505 507 else:
506 508 self.active_eventloop = self._inputhook = None
507 509
508 510 # Run !system commands directly, not through pipes, so terminal programs
509 511 # work correctly.
510 512 system = InteractiveShell.system_raw
511 513
512 514 def auto_rewrite_input(self, cmd):
513 515 """Overridden from the parent class to use fancy rewriting prompt"""
514 516 if not self.show_rewritten_input:
515 517 return
516 518
517 519 tokens = self.prompts.rewrite_prompt_tokens()
518 520 if self.pt_cli:
519 521 self.pt_cli.print_tokens(tokens)
520 522 print(cmd)
521 523 else:
522 524 prompt = ''.join(s for t, s in tokens)
523 525 print(prompt, cmd, sep='')
524 526
525 527 _prompts_before = None
526 528 def switch_doctest_mode(self, mode):
527 529 """Switch prompts to classic for %doctest_mode"""
528 530 if mode:
529 531 self._prompts_before = self.prompts
530 532 self.prompts = ClassicPrompts(self)
531 533 elif self._prompts_before:
532 534 self.prompts = self._prompts_before
533 535 self._prompts_before = None
534 536 self._update_layout()
535 537
536 538
537 539 InteractiveShellABC.register(TerminalInteractiveShell)
538 540
539 541 if __name__ == '__main__':
540 542 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now