##// END OF EJS Templates
Merge pull request #9501 from takluyver/bracket-match-default-on...
Matthias Bussonnier -
r22376:cb5bb6b4 merge
parent child Browse files
Show More
@@ -1,481 +1,481 b''
1 1 """IPython terminal interface using prompt_toolkit in place of readline"""
2 2 from __future__ import print_function
3 3
4 4 import os
5 5 import sys
6 6 import signal
7 7 import unicodedata
8 8 from warnings import warn
9 9 from wcwidth import wcwidth
10 10
11 11 from IPython.core.error import TryNext
12 12 from IPython.core.interactiveshell import InteractiveShell
13 13 from IPython.utils.py3compat import PY3, cast_unicode_py2, input
14 14 from IPython.utils.terminal import toggle_set_term_title, set_term_title
15 15 from IPython.utils.process import abbrev_cwd
16 16 from traitlets import Bool, Unicode, Dict, Integer, observe
17 17
18 18 from prompt_toolkit.completion import Completer, Completion
19 19 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER, EditingMode
20 20 from prompt_toolkit.filters import HasFocus, HasSelection, Condition, ViInsertMode, EmacsInsertMode, IsDone
21 21 from prompt_toolkit.history import InMemoryHistory
22 22 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop, create_prompt_layout
23 23 from prompt_toolkit.interface import CommandLineInterface
24 24 from prompt_toolkit.key_binding.manager import KeyBindingManager
25 25 from prompt_toolkit.keys import Keys
26 26 from prompt_toolkit.layout.lexers import Lexer
27 27 from prompt_toolkit.layout.lexers import PygmentsLexer
28 28 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
29 29 from prompt_toolkit.styles import PygmentsStyle, DynamicStyle
30 30
31 31 from pygments.styles import get_style_by_name, get_all_styles
32 32 from pygments.lexers import Python3Lexer, BashLexer, PythonLexer
33 33 from pygments.token import Token
34 34
35 35 from .pt_inputhooks import get_inputhook_func
36 36 from .interactiveshell import get_default_editor, TerminalMagics
37 37
38 38
39 39 class IPythonPTCompleter(Completer):
40 40 """Adaptor to provide IPython completions to prompt_toolkit"""
41 41 def __init__(self, ipy_completer):
42 42 self.ipy_completer = ipy_completer
43 43
44 44 def get_completions(self, document, complete_event):
45 45 if not document.current_line.strip():
46 46 return
47 47
48 48 used, matches = self.ipy_completer.complete(
49 49 line_buffer=document.current_line,
50 50 cursor_pos=document.cursor_position_col
51 51 )
52 52 start_pos = -len(used)
53 53 for m in matches:
54 54 m = unicodedata.normalize('NFC', m)
55 55
56 56 # When the first character of the completion has a zero length,
57 57 # then it's probably a decomposed unicode character. E.g. caused by
58 58 # the "\dot" completion. Try to compose again with the previous
59 59 # character.
60 60 if wcwidth(m[0]) == 0:
61 61 if document.cursor_position + start_pos > 0:
62 62 char_before = document.text[document.cursor_position + start_pos - 1]
63 63 m = unicodedata.normalize('NFC', char_before + m)
64 64
65 65 # Yield the modified completion instead, if this worked.
66 66 if wcwidth(m[0:1]) == 1:
67 67 yield Completion(m, start_position=start_pos - 1)
68 68 continue
69 69
70 70 # TODO: Use Jedi to determine meta_text
71 71 # (Jedi currently has a bug that results in incorrect information.)
72 72 # meta_text = ''
73 73 # yield Completion(m, start_position=start_pos,
74 74 # display_meta=meta_text)
75 75 yield Completion(m, start_position=start_pos)
76 76
77 77 class IPythonPTLexer(Lexer):
78 78 """
79 79 Wrapper around PythonLexer and BashLexer.
80 80 """
81 81 def __init__(self):
82 82 self.python_lexer = PygmentsLexer(Python3Lexer if PY3 else PythonLexer)
83 83 self.shell_lexer = PygmentsLexer(BashLexer)
84 84
85 85 def lex_document(self, cli, document):
86 86 if document.text.startswith('!'):
87 87 return self.shell_lexer.lex_document(cli, document)
88 88 else:
89 89 return self.python_lexer.lex_document(cli, document)
90 90
91 91
92 92 class TerminalInteractiveShell(InteractiveShell):
93 93 colors_force = True
94 94
95 95 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
96 96 'to reserve for the completion menu'
97 97 ).tag(config=True)
98 98
99 99 def _space_for_menu_changed(self, old, new):
100 100 self._update_layout()
101 101
102 102 pt_cli = None
103 103
104 104 autoedit_syntax = Bool(False,
105 105 help="auto editing of files with syntax errors.",
106 106 ).tag(config=True)
107 107
108 108
109 109 confirm_exit = Bool(True,
110 110 help="""
111 111 Set to confirm when you try to exit IPython with an EOF (Control-D
112 112 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
113 113 you can force a direct exit without any confirmation.""",
114 114 ).tag(config=True)
115 115
116 116 editing_mode = Unicode('emacs',
117 117 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
118 118 ).tag(config=True)
119 119
120 120 mouse_support = Bool(False,
121 121 help="Enable mouse support in the prompt"
122 122 ).tag(config=True)
123 123
124 124 highlighting_style = Unicode('default',
125 125 help="The name of a Pygments style to use for syntax highlighting: \n %s" % ', '.join(get_all_styles())
126 126 ).tag(config=True)
127 127
128 128
129 129 @observe('highlighting_style')
130 130 def _highlighting_style_changed(self, change):
131 131 self._style = self._make_style_from_name(self.highlighting_style)
132 132
133 133 highlighting_style_overrides = Dict(
134 134 help="Override highlighting format for specific tokens"
135 135 ).tag(config=True)
136 136
137 137 editor = Unicode(get_default_editor(),
138 138 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
139 139 ).tag(config=True)
140 140
141 141 term_title = Bool(True,
142 142 help="Automatically set the terminal title"
143 143 ).tag(config=True)
144 144
145 145 display_completions_in_columns = Bool(False,
146 146 help="Display a multi column completion menu.",
147 147 ).tag(config=True)
148 148
149 highlight_matching_brackets = Bool(False,
149 highlight_matching_brackets = Bool(True,
150 150 help="Highlight matching brackets .",
151 151 ).tag(config=True)
152 152
153 153 @observe('term_title')
154 154 def init_term_title(self, change=None):
155 155 # Enable or disable the terminal title.
156 156 if self.term_title:
157 157 toggle_set_term_title(True)
158 158 set_term_title('IPython: ' + abbrev_cwd())
159 159 else:
160 160 toggle_set_term_title(False)
161 161
162 162 def get_prompt_tokens(self, cli):
163 163 return [
164 164 (Token.Prompt, 'In ['),
165 165 (Token.PromptNum, str(self.execution_count)),
166 166 (Token.Prompt, ']: '),
167 167 ]
168 168
169 169 def get_continuation_tokens(self, cli, width):
170 170 return [
171 171 (Token.Prompt, (' ' * (width - 5)) + '...: '),
172 172 ]
173 173
174 174 def init_prompt_toolkit_cli(self):
175 175 if ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or not sys.stdin.isatty():
176 176 # Fall back to plain non-interactive output for tests.
177 177 # This is very limited, and only accepts a single line.
178 178 def prompt():
179 179 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
180 180 self.prompt_for_code = prompt
181 181 return
182 182
183 183 kbmanager = KeyBindingManager.for_prompt()
184 184 insert_mode = ViInsertMode() | EmacsInsertMode()
185 185 # Ctrl+J == Enter, seemingly
186 186 @kbmanager.registry.add_binding(Keys.ControlJ,
187 187 filter=(HasFocus(DEFAULT_BUFFER)
188 188 & ~HasSelection()
189 189 & insert_mode
190 190 ))
191 191 def _(event):
192 192 b = event.current_buffer
193 193 d = b.document
194 194 if not (d.on_last_line or d.cursor_position_row >= d.line_count
195 195 - d.empty_line_count_at_the_end()):
196 196 b.newline()
197 197 return
198 198
199 199 status, indent = self.input_splitter.check_complete(d.text)
200 200
201 201 if (status != 'incomplete') and b.accept_action.is_returnable:
202 202 b.accept_action.validate_and_handle(event.cli, b)
203 203 else:
204 204 b.insert_text('\n' + (' ' * (indent or 0)))
205 205
206 206 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(DEFAULT_BUFFER))
207 207 def _reset_buffer(event):
208 208 event.current_buffer.reset()
209 209
210 210 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(SEARCH_BUFFER))
211 211 def _reset_search_buffer(event):
212 212 if event.current_buffer.document.text:
213 213 event.current_buffer.reset()
214 214 else:
215 215 event.cli.push_focus(DEFAULT_BUFFER)
216 216
217 217 supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP'))
218 218
219 219 @kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend)
220 220 def _suspend_to_bg(event):
221 221 event.cli.suspend_to_background()
222 222
223 223 @Condition
224 224 def cursor_in_leading_ws(cli):
225 225 before = cli.application.buffer.document.current_line_before_cursor
226 226 return (not before) or before.isspace()
227 227
228 228 # Ctrl+I == Tab
229 229 @kbmanager.registry.add_binding(Keys.ControlI,
230 230 filter=(HasFocus(DEFAULT_BUFFER)
231 231 & ~HasSelection()
232 232 & insert_mode
233 233 & cursor_in_leading_ws
234 234 ))
235 235 def _indent_buffer(event):
236 236 event.current_buffer.insert_text(' ' * 4)
237 237
238 238 # Pre-populate history from IPython's history database
239 239 history = InMemoryHistory()
240 240 last_cell = u""
241 241 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
242 242 include_latest=True):
243 243 # Ignore blank lines and consecutive duplicates
244 244 cell = cell.rstrip()
245 245 if cell and (cell != last_cell):
246 246 history.append(cell)
247 247
248 248 self._style = self._make_style_from_name(self.highlighting_style)
249 249 style = DynamicStyle(lambda: self._style)
250 250
251 251 editing_mode = getattr(EditingMode, self.editing_mode.upper())
252 252
253 253 self._app = create_prompt_application(
254 254 editing_mode=editing_mode,
255 255 key_bindings_registry=kbmanager.registry,
256 256 history=history,
257 257 completer=IPythonPTCompleter(self.Completer),
258 258 enable_history_search=True,
259 259 style=style,
260 260 mouse_support=self.mouse_support,
261 261 **self._layout_options()
262 262 )
263 263 self._eventloop = create_eventloop(self.inputhook)
264 264 self.pt_cli = CommandLineInterface(self._app, eventloop=self._eventloop)
265 265
266 266 def _make_style_from_name(self, name):
267 267 """
268 268 Small wrapper that make an IPython compatible style from a style name
269 269
270 270 We need that to add style for prompt ... etc.
271 271 """
272 272 style_cls = get_style_by_name(name)
273 273 style_overrides = {
274 274 Token.Prompt: '#009900',
275 275 Token.PromptNum: '#00ff00 bold',
276 276 }
277 277 if name == 'default':
278 278 style_cls = get_style_by_name('default')
279 279 # The default theme needs to be visible on both a dark background
280 280 # and a light background, because we can't tell what the terminal
281 281 # looks like. These tweaks to the default theme help with that.
282 282 style_overrides.update({
283 283 Token.Number: '#007700',
284 284 Token.Operator: 'noinherit',
285 285 Token.String: '#BB6622',
286 286 Token.Name.Function: '#2080D0',
287 287 Token.Name.Class: 'bold #2080D0',
288 288 Token.Name.Namespace: 'bold #2080D0',
289 289 })
290 290 style_overrides.update(self.highlighting_style_overrides)
291 291 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
292 292 style_dict=style_overrides)
293 293
294 294 return style
295 295
296 296 def _layout_options(self):
297 297 """
298 298 Return the current layout option for the current Terminal InteractiveShell
299 299 """
300 300 return {
301 301 'lexer':IPythonPTLexer(),
302 302 'reserve_space_for_menu':self.space_for_menu,
303 303 'get_prompt_tokens':self.get_prompt_tokens,
304 304 'get_continuation_tokens':self.get_continuation_tokens,
305 305 'multiline':True,
306 306 'display_completions_in_columns': self.display_completions_in_columns,
307 307
308 308 # Highlight matching brackets, but only when this setting is
309 309 # enabled, and only when the DEFAULT_BUFFER has the focus.
310 310 'extra_input_processors': [ConditionalProcessor(
311 311 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
312 312 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
313 313 Condition(lambda cli: self.highlight_matching_brackets))],
314 314 }
315 315
316 316 def _update_layout(self):
317 317 """
318 318 Ask for a re computation of the application layout, if for example ,
319 319 some configuration options have changed.
320 320 """
321 321 self._app.layout = create_prompt_layout(**self._layout_options())
322 322
323 323 def prompt_for_code(self):
324 324 document = self.pt_cli.run(
325 325 pre_run=self.pre_prompt, reset_current_buffer=True)
326 326 return document.text
327 327
328 328 def init_io(self):
329 329 if sys.platform not in {'win32', 'cli'}:
330 330 return
331 331
332 332 import colorama
333 333 colorama.init()
334 334
335 335 # For some reason we make these wrappers around stdout/stderr.
336 336 # For now, we need to reset them so all output gets coloured.
337 337 # https://github.com/ipython/ipython/issues/8669
338 338 from IPython.utils import io
339 339 io.stdout = io.IOStream(sys.stdout)
340 340 io.stderr = io.IOStream(sys.stderr)
341 341
342 342 def init_magics(self):
343 343 super(TerminalInteractiveShell, self).init_magics()
344 344 self.register_magics(TerminalMagics)
345 345
346 346 def init_alias(self):
347 347 # The parent class defines aliases that can be safely used with any
348 348 # frontend.
349 349 super(TerminalInteractiveShell, self).init_alias()
350 350
351 351 # Now define aliases that only make sense on the terminal, because they
352 352 # need direct access to the console in a way that we can't emulate in
353 353 # GUI or web frontend
354 354 if os.name == 'posix':
355 355 for cmd in ['clear', 'more', 'less', 'man']:
356 356 self.alias_manager.soft_define_alias(cmd, cmd)
357 357
358 358
359 359 def __init__(self, *args, **kwargs):
360 360 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
361 361 self.init_prompt_toolkit_cli()
362 362 self.init_term_title()
363 363 self.keep_running = True
364 364
365 365 def ask_exit(self):
366 366 self.keep_running = False
367 367
368 368 rl_next_input = None
369 369
370 370 def pre_prompt(self):
371 371 if self.rl_next_input:
372 372 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
373 373 self.rl_next_input = None
374 374
375 375 def interact(self):
376 376 while self.keep_running:
377 377 print(self.separate_in, end='')
378 378
379 379 try:
380 380 code = self.prompt_for_code()
381 381 except EOFError:
382 382 if (not self.confirm_exit) \
383 383 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
384 384 self.ask_exit()
385 385
386 386 else:
387 387 if code:
388 388 self.run_cell(code, store_history=True)
389 389 if self.autoedit_syntax and self.SyntaxTB.last_syntax_error:
390 390 self.edit_syntax_error()
391 391
392 392 def mainloop(self):
393 393 # An extra layer of protection in case someone mashing Ctrl-C breaks
394 394 # out of our internal code.
395 395 while True:
396 396 try:
397 397 self.interact()
398 398 break
399 399 except KeyboardInterrupt:
400 400 print("\nKeyboardInterrupt escaped interact()\n")
401 401
402 402 if hasattr(self, '_eventloop'):
403 403 self._eventloop.close()
404 404
405 405 _inputhook = None
406 406 def inputhook(self, context):
407 407 if self._inputhook is not None:
408 408 self._inputhook(context)
409 409
410 410 def enable_gui(self, gui=None):
411 411 if gui:
412 412 self._inputhook = get_inputhook_func(gui)
413 413 else:
414 414 self._inputhook = None
415 415
416 416 # Methods to support auto-editing of SyntaxErrors:
417 417
418 418 def edit_syntax_error(self):
419 419 """The bottom half of the syntax error handler called in the main loop.
420 420
421 421 Loop until syntax error is fixed or user cancels.
422 422 """
423 423
424 424 while self.SyntaxTB.last_syntax_error:
425 425 # copy and clear last_syntax_error
426 426 err = self.SyntaxTB.clear_err_state()
427 427 if not self._should_recompile(err):
428 428 return
429 429 try:
430 430 # may set last_syntax_error again if a SyntaxError is raised
431 431 self.safe_execfile(err.filename, self.user_ns)
432 432 except:
433 433 self.showtraceback()
434 434 else:
435 435 try:
436 436 with open(err.filename) as f:
437 437 # This should be inside a display_trap block and I
438 438 # think it is.
439 439 sys.displayhook(f.read())
440 440 except:
441 441 self.showtraceback()
442 442
443 443 def _should_recompile(self, e):
444 444 """Utility routine for edit_syntax_error"""
445 445
446 446 if e.filename in ('<ipython console>', '<input>', '<string>',
447 447 '<console>', '<BackgroundJob compilation>',
448 448 None):
449 449 return False
450 450 try:
451 451 if (self.autoedit_syntax and
452 452 not self.ask_yes_no(
453 453 'Return to editor to correct syntax error? '
454 454 '[Y/n] ', 'y')):
455 455 return False
456 456 except EOFError:
457 457 return False
458 458
459 459 def int0(x):
460 460 try:
461 461 return int(x)
462 462 except TypeError:
463 463 return 0
464 464
465 465 # always pass integer line and offset values to editor hook
466 466 try:
467 467 self.hooks.fix_error_editor(e.filename,
468 468 int0(e.lineno), int0(e.offset),
469 469 e.msg)
470 470 except TryNext:
471 471 warn('Could not open editor')
472 472 return False
473 473 return True
474 474
475 475 # Run !system commands directly, not through pipes, so terminal programs
476 476 # work correctly.
477 477 system = InteractiveShell.system_raw
478 478
479 479
480 480 if __name__ == '__main__':
481 481 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now