##// END OF EJS Templates
Do not redefine _, linter complain....
Matthias Bussonnier -
Show More
@@ -1,462 +1,462 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 def _(event):
198 def _reset_buffer(event):
199 199 event.current_buffer.reset()
200 200
201 201 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(SEARCH_BUFFER))
202 def _(event):
202 def _reset_search_buffer(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 def _(event):
211 def _suspend_to_bg(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 def _(event):
226 def _indent_buffer(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 for _, _, cell in self.history_manager.get_tail(self.history_load_length,
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.pt_cli = CommandLineInterface(self._app,
255 255 eventloop=create_eventloop(self.inputhook))
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 == '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 385
386 386 _inputhook = None
387 387 def inputhook(self, context):
388 388 if self._inputhook is not None:
389 389 self._inputhook(context)
390 390
391 391 def enable_gui(self, gui=None):
392 392 if gui:
393 393 self._inputhook = get_inputhook_func(gui)
394 394 else:
395 395 self._inputhook = None
396 396
397 397 # Methods to support auto-editing of SyntaxErrors:
398 398
399 399 def edit_syntax_error(self):
400 400 """The bottom half of the syntax error handler called in the main loop.
401 401
402 402 Loop until syntax error is fixed or user cancels.
403 403 """
404 404
405 405 while self.SyntaxTB.last_syntax_error:
406 406 # copy and clear last_syntax_error
407 407 err = self.SyntaxTB.clear_err_state()
408 408 if not self._should_recompile(err):
409 409 return
410 410 try:
411 411 # may set last_syntax_error again if a SyntaxError is raised
412 412 self.safe_execfile(err.filename, self.user_ns)
413 413 except:
414 414 self.showtraceback()
415 415 else:
416 416 try:
417 417 with open(err.filename) as f:
418 418 # This should be inside a display_trap block and I
419 419 # think it is.
420 420 sys.displayhook(f.read())
421 421 except:
422 422 self.showtraceback()
423 423
424 424 def _should_recompile(self, e):
425 425 """Utility routine for edit_syntax_error"""
426 426
427 427 if e.filename in ('<ipython console>', '<input>', '<string>',
428 428 '<console>', '<BackgroundJob compilation>',
429 429 None):
430 430 return False
431 431 try:
432 432 if (self.autoedit_syntax and
433 433 not self.ask_yes_no(
434 434 'Return to editor to correct syntax error? '
435 435 '[Y/n] ', 'y')):
436 436 return False
437 437 except EOFError:
438 438 return False
439 439
440 440 def int0(x):
441 441 try:
442 442 return int(x)
443 443 except TypeError:
444 444 return 0
445 445
446 446 # always pass integer line and offset values to editor hook
447 447 try:
448 448 self.hooks.fix_error_editor(e.filename,
449 449 int0(e.lineno), int0(e.offset),
450 450 e.msg)
451 451 except TryNext:
452 452 warn('Could not open editor')
453 453 return False
454 454 return True
455 455
456 456 # Run !system commands directly, not through pipes, so terminal programs
457 457 # work correctly.
458 458 system = InteractiveShell.system_raw
459 459
460 460
461 461 if __name__ == '__main__':
462 462 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now