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