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