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