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