##// END OF EJS Templates
Merge pull request #9360 from takluyver/i9359...
Thomas Kluyver -
r22203:bad8a12c merge
parent child Browse files
Show More
@@ -1,314 +1,321
1 """IPython terminal interface using prompt_toolkit in place of readline"""
1 """IPython terminal interface using prompt_toolkit in place of readline"""
2 from __future__ import print_function
2 from __future__ import print_function
3
3
4 import os
4 import os
5 import sys
5 import sys
6 import signal
6 import signal
7
7
8 from IPython.core.interactiveshell import InteractiveShell
8 from IPython.core.interactiveshell import InteractiveShell
9 from IPython.utils.py3compat import PY3, cast_unicode_py2, input
9 from IPython.utils.py3compat import PY3, cast_unicode_py2, input
10 from IPython.utils.terminal import toggle_set_term_title, set_term_title
10 from IPython.utils.terminal import toggle_set_term_title, set_term_title
11 from IPython.utils.process import abbrev_cwd
11 from IPython.utils.process import abbrev_cwd
12 from traitlets import Bool, Unicode, Dict
12 from traitlets import Bool, CBool, Unicode, Dict
13
13
14 from prompt_toolkit.completion import Completer, Completion
14 from prompt_toolkit.completion import Completer, Completion
15 from prompt_toolkit.enums import DEFAULT_BUFFER
15 from prompt_toolkit.enums import DEFAULT_BUFFER
16 from prompt_toolkit.filters import HasFocus, HasSelection, Condition
16 from prompt_toolkit.filters import HasFocus, HasSelection, Condition
17 from prompt_toolkit.history import InMemoryHistory
17 from prompt_toolkit.history import InMemoryHistory
18 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop
18 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop
19 from prompt_toolkit.interface import CommandLineInterface
19 from prompt_toolkit.interface import CommandLineInterface
20 from prompt_toolkit.key_binding.manager import KeyBindingManager
20 from prompt_toolkit.key_binding.manager import KeyBindingManager
21 from prompt_toolkit.key_binding.vi_state import InputMode
21 from prompt_toolkit.key_binding.vi_state import InputMode
22 from prompt_toolkit.key_binding.bindings.vi import ViStateFilter
22 from prompt_toolkit.key_binding.bindings.vi import ViStateFilter
23 from prompt_toolkit.keys import Keys
23 from prompt_toolkit.keys import Keys
24 from prompt_toolkit.layout.lexers import Lexer
24 from prompt_toolkit.layout.lexers import Lexer
25 from prompt_toolkit.layout.lexers import PygmentsLexer
25 from prompt_toolkit.layout.lexers import PygmentsLexer
26 from prompt_toolkit.styles import PygmentsStyle
26 from prompt_toolkit.styles import PygmentsStyle
27
27
28 from pygments.styles import get_style_by_name
28 from pygments.styles import get_style_by_name
29 from pygments.lexers import Python3Lexer, BashLexer, PythonLexer
29 from pygments.lexers import Python3Lexer, BashLexer, PythonLexer
30 from pygments.token import Token
30 from pygments.token import Token
31
31
32 from .pt_inputhooks import get_inputhook_func
32 from .pt_inputhooks import get_inputhook_func
33 from .interactiveshell import get_default_editor, TerminalMagics
33 from .interactiveshell import get_default_editor, TerminalMagics
34
34
35
35
36
36
37 class IPythonPTCompleter(Completer):
37 class IPythonPTCompleter(Completer):
38 """Adaptor to provide IPython completions to prompt_toolkit"""
38 """Adaptor to provide IPython completions to prompt_toolkit"""
39 def __init__(self, ipy_completer):
39 def __init__(self, ipy_completer):
40 self.ipy_completer = ipy_completer
40 self.ipy_completer = ipy_completer
41
41
42 def get_completions(self, document, complete_event):
42 def get_completions(self, document, complete_event):
43 if not document.current_line.strip():
43 if not document.current_line.strip():
44 return
44 return
45
45
46 used, matches = self.ipy_completer.complete(
46 used, matches = self.ipy_completer.complete(
47 line_buffer=document.current_line,
47 line_buffer=document.current_line,
48 cursor_pos=document.cursor_position_col
48 cursor_pos=document.cursor_position_col
49 )
49 )
50 start_pos = -len(used)
50 start_pos = -len(used)
51 for m in matches:
51 for m in matches:
52 yield Completion(m, start_position=start_pos)
52 yield Completion(m, start_position=start_pos)
53
53
54
54
55 class IPythonPTLexer(Lexer):
55 class IPythonPTLexer(Lexer):
56 """
56 """
57 Wrapper around PythonLexer and BashLexer.
57 Wrapper around PythonLexer and BashLexer.
58 """
58 """
59 def __init__(self):
59 def __init__(self):
60 self.python_lexer = PygmentsLexer(Python3Lexer if PY3 else PythonLexer)
60 self.python_lexer = PygmentsLexer(Python3Lexer if PY3 else PythonLexer)
61 self.shell_lexer = PygmentsLexer(BashLexer)
61 self.shell_lexer = PygmentsLexer(BashLexer)
62
62
63 def lex_document(self, cli, document):
63 def lex_document(self, cli, document):
64 if document.text.startswith('!'):
64 if document.text.startswith('!'):
65 return self.shell_lexer.lex_document(cli, document)
65 return self.shell_lexer.lex_document(cli, document)
66 else:
66 else:
67 return self.python_lexer.lex_document(cli, document)
67 return self.python_lexer.lex_document(cli, document)
68
68
69
69
70 class TerminalInteractiveShell(InteractiveShell):
70 class TerminalInteractiveShell(InteractiveShell):
71 colors_force = True
71 colors_force = True
72
72
73 pt_cli = None
73 pt_cli = None
74
74
75 confirm_exit = CBool(True, config=True,
76 help="""
77 Set to confirm when you try to exit IPython with an EOF (Control-D
78 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
79 you can force a direct exit without any confirmation.""",
80 )
75 vi_mode = Bool(False, config=True,
81 vi_mode = Bool(False, config=True,
76 help="Use vi style keybindings at the prompt",
82 help="Use vi style keybindings at the prompt",
77 )
83 )
78
84
79 mouse_support = Bool(False, config=True,
85 mouse_support = Bool(False, config=True,
80 help="Enable mouse support in the prompt"
86 help="Enable mouse support in the prompt"
81 )
87 )
82
88
83 highlighting_style = Unicode('', config=True,
89 highlighting_style = Unicode('', config=True,
84 help="The name of a Pygments style to use for syntax highlighting"
90 help="The name of a Pygments style to use for syntax highlighting"
85 )
91 )
86
92
87 highlighting_style_overrides = Dict(config=True,
93 highlighting_style_overrides = Dict(config=True,
88 help="Override highlighting format for specific tokens"
94 help="Override highlighting format for specific tokens"
89 )
95 )
90
96
91 editor = Unicode(get_default_editor(), config=True,
97 editor = Unicode(get_default_editor(), config=True,
92 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
98 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
93 )
99 )
94
100
95 term_title = Bool(True, config=True,
101 term_title = Bool(True, config=True,
96 help="Automatically set the terminal title"
102 help="Automatically set the terminal title"
97 )
103 )
98 def _term_title_changed(self, name, new_value):
104 def _term_title_changed(self, name, new_value):
99 self.init_term_title()
105 self.init_term_title()
100
106
101 def init_term_title(self):
107 def init_term_title(self):
102 # Enable or disable the terminal title.
108 # Enable or disable the terminal title.
103 if self.term_title:
109 if self.term_title:
104 toggle_set_term_title(True)
110 toggle_set_term_title(True)
105 set_term_title('IPython: ' + abbrev_cwd())
111 set_term_title('IPython: ' + abbrev_cwd())
106 else:
112 else:
107 toggle_set_term_title(False)
113 toggle_set_term_title(False)
108
114
109 def get_prompt_tokens(self, cli):
115 def get_prompt_tokens(self, cli):
110 return [
116 return [
111 (Token.Prompt, 'In ['),
117 (Token.Prompt, 'In ['),
112 (Token.PromptNum, str(self.execution_count)),
118 (Token.PromptNum, str(self.execution_count)),
113 (Token.Prompt, ']: '),
119 (Token.Prompt, ']: '),
114 ]
120 ]
115
121
116 def get_continuation_tokens(self, cli, width):
122 def get_continuation_tokens(self, cli, width):
117 return [
123 return [
118 (Token.Prompt, (' ' * (width - 5)) + '...: '),
124 (Token.Prompt, (' ' * (width - 5)) + '...: '),
119 ]
125 ]
120
126
121 def init_prompt_toolkit_cli(self):
127 def init_prompt_toolkit_cli(self):
122 if ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or not sys.stdin.isatty():
128 if ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or not sys.stdin.isatty():
123 # Fall back to plain non-interactive output for tests.
129 # Fall back to plain non-interactive output for tests.
124 # This is very limited, and only accepts a single line.
130 # This is very limited, and only accepts a single line.
125 def prompt():
131 def prompt():
126 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
132 return cast_unicode_py2(input('In [%d]: ' % self.execution_count))
127 self.prompt_for_code = prompt
133 self.prompt_for_code = prompt
128 return
134 return
129
135
130 kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode)
136 kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode)
131 insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT)
137 insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT)
132 # Ctrl+J == Enter, seemingly
138 # Ctrl+J == Enter, seemingly
133 @kbmanager.registry.add_binding(Keys.ControlJ,
139 @kbmanager.registry.add_binding(Keys.ControlJ,
134 filter=(HasFocus(DEFAULT_BUFFER)
140 filter=(HasFocus(DEFAULT_BUFFER)
135 & ~HasSelection()
141 & ~HasSelection()
136 & insert_mode
142 & insert_mode
137 ))
143 ))
138 def _(event):
144 def _(event):
139 b = event.current_buffer
145 b = event.current_buffer
140 d = b.document
146 d = b.document
141 if not (d.on_last_line or d.cursor_position_row >= d.line_count
147 if not (d.on_last_line or d.cursor_position_row >= d.line_count
142 - d.empty_line_count_at_the_end()):
148 - d.empty_line_count_at_the_end()):
143 b.newline()
149 b.newline()
144 return
150 return
145
151
146 status, indent = self.input_splitter.check_complete(d.text)
152 status, indent = self.input_splitter.check_complete(d.text)
147
153
148 if (status != 'incomplete') and b.accept_action.is_returnable:
154 if (status != 'incomplete') and b.accept_action.is_returnable:
149 b.accept_action.validate_and_handle(event.cli, b)
155 b.accept_action.validate_and_handle(event.cli, b)
150 else:
156 else:
151 b.insert_text('\n' + (' ' * (indent or 0)))
157 b.insert_text('\n' + (' ' * (indent or 0)))
152
158
153 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(DEFAULT_BUFFER))
159 @kbmanager.registry.add_binding(Keys.ControlC, filter=HasFocus(DEFAULT_BUFFER))
154 def _(event):
160 def _(event):
155 event.current_buffer.reset()
161 event.current_buffer.reset()
156
162
157 supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP'))
163 supports_suspend = Condition(lambda cli: hasattr(signal, 'SIGTSTP'))
158
164
159 @kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend)
165 @kbmanager.registry.add_binding(Keys.ControlZ, filter=supports_suspend)
160 def _(event):
166 def _(event):
161 event.cli.suspend_to_background()
167 event.cli.suspend_to_background()
162
168
163 @Condition
169 @Condition
164 def cursor_in_leading_ws(cli):
170 def cursor_in_leading_ws(cli):
165 before = cli.application.buffer.document.current_line_before_cursor
171 before = cli.application.buffer.document.current_line_before_cursor
166 return (not before) or before.isspace()
172 return (not before) or before.isspace()
167
173
168 # Ctrl+I == Tab
174 # Ctrl+I == Tab
169 @kbmanager.registry.add_binding(Keys.ControlI,
175 @kbmanager.registry.add_binding(Keys.ControlI,
170 filter=(HasFocus(DEFAULT_BUFFER)
176 filter=(HasFocus(DEFAULT_BUFFER)
171 & ~HasSelection()
177 & ~HasSelection()
172 & insert_mode
178 & insert_mode
173 & cursor_in_leading_ws
179 & cursor_in_leading_ws
174 ))
180 ))
175 def _(event):
181 def _(event):
176 event.current_buffer.insert_text(' ' * 4)
182 event.current_buffer.insert_text(' ' * 4)
177
183
178 # Pre-populate history from IPython's history database
184 # Pre-populate history from IPython's history database
179 history = InMemoryHistory()
185 history = InMemoryHistory()
180 last_cell = u""
186 last_cell = u""
181 for _, _, cell in self.history_manager.get_tail(self.history_load_length,
187 for _, _, cell in self.history_manager.get_tail(self.history_load_length,
182 include_latest=True):
188 include_latest=True):
183 # Ignore blank lines and consecutive duplicates
189 # Ignore blank lines and consecutive duplicates
184 cell = cell.rstrip()
190 cell = cell.rstrip()
185 if cell and (cell != last_cell):
191 if cell and (cell != last_cell):
186 history.append(cell)
192 history.append(cell)
187
193
188 style_overrides = {
194 style_overrides = {
189 Token.Prompt: '#009900',
195 Token.Prompt: '#009900',
190 Token.PromptNum: '#00ff00 bold',
196 Token.PromptNum: '#00ff00 bold',
191 }
197 }
192 if self.highlighting_style:
198 if self.highlighting_style:
193 style_cls = get_style_by_name(self.highlighting_style)
199 style_cls = get_style_by_name(self.highlighting_style)
194 else:
200 else:
195 style_cls = get_style_by_name('default')
201 style_cls = get_style_by_name('default')
196 # The default theme needs to be visible on both a dark background
202 # The default theme needs to be visible on both a dark background
197 # and a light background, because we can't tell what the terminal
203 # and a light background, because we can't tell what the terminal
198 # looks like. These tweaks to the default theme help with that.
204 # looks like. These tweaks to the default theme help with that.
199 style_overrides.update({
205 style_overrides.update({
200 Token.Number: '#007700',
206 Token.Number: '#007700',
201 Token.Operator: 'noinherit',
207 Token.Operator: 'noinherit',
202 Token.String: '#BB6622',
208 Token.String: '#BB6622',
203 Token.Name.Function: '#2080D0',
209 Token.Name.Function: '#2080D0',
204 Token.Name.Class: 'bold #2080D0',
210 Token.Name.Class: 'bold #2080D0',
205 Token.Name.Namespace: 'bold #2080D0',
211 Token.Name.Namespace: 'bold #2080D0',
206 })
212 })
207 style_overrides.update(self.highlighting_style_overrides)
213 style_overrides.update(self.highlighting_style_overrides)
208 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
214 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
209 style_dict=style_overrides)
215 style_dict=style_overrides)
210
216
211 app = create_prompt_application(multiline=True,
217 app = create_prompt_application(multiline=True,
212 lexer=IPythonPTLexer(),
218 lexer=IPythonPTLexer(),
213 get_prompt_tokens=self.get_prompt_tokens,
219 get_prompt_tokens=self.get_prompt_tokens,
214 get_continuation_tokens=self.get_continuation_tokens,
220 get_continuation_tokens=self.get_continuation_tokens,
215 key_bindings_registry=kbmanager.registry,
221 key_bindings_registry=kbmanager.registry,
216 history=history,
222 history=history,
217 completer=IPythonPTCompleter(self.Completer),
223 completer=IPythonPTCompleter(self.Completer),
218 enable_history_search=True,
224 enable_history_search=True,
219 style=style,
225 style=style,
220 mouse_support=self.mouse_support,
226 mouse_support=self.mouse_support,
221 reserve_space_for_menu=6,
227 reserve_space_for_menu=6,
222 )
228 )
223
229
224 self.pt_cli = CommandLineInterface(app,
230 self.pt_cli = CommandLineInterface(app,
225 eventloop=create_eventloop(self.inputhook))
231 eventloop=create_eventloop(self.inputhook))
226
232
227 def prompt_for_code(self):
233 def prompt_for_code(self):
228 document = self.pt_cli.run(pre_run=self.pre_prompt)
234 document = self.pt_cli.run(pre_run=self.pre_prompt)
229 return document.text
235 return document.text
230
236
231 def init_io(self):
237 def init_io(self):
232 if sys.platform not in {'win32', 'cli'}:
238 if sys.platform not in {'win32', 'cli'}:
233 return
239 return
234
240
235 import colorama
241 import colorama
236 colorama.init()
242 colorama.init()
237
243
238 # For some reason we make these wrappers around stdout/stderr.
244 # For some reason we make these wrappers around stdout/stderr.
239 # For now, we need to reset them so all output gets coloured.
245 # For now, we need to reset them so all output gets coloured.
240 # https://github.com/ipython/ipython/issues/8669
246 # https://github.com/ipython/ipython/issues/8669
241 from IPython.utils import io
247 from IPython.utils import io
242 io.stdout = io.IOStream(sys.stdout)
248 io.stdout = io.IOStream(sys.stdout)
243 io.stderr = io.IOStream(sys.stderr)
249 io.stderr = io.IOStream(sys.stderr)
244
250
245 def init_magics(self):
251 def init_magics(self):
246 super(TerminalInteractiveShell, self).init_magics()
252 super(TerminalInteractiveShell, self).init_magics()
247 self.register_magics(TerminalMagics)
253 self.register_magics(TerminalMagics)
248
254
249 def init_alias(self):
255 def init_alias(self):
250 # The parent class defines aliases that can be safely used with any
256 # The parent class defines aliases that can be safely used with any
251 # frontend.
257 # frontend.
252 super(TerminalInteractiveShell, self).init_alias()
258 super(TerminalInteractiveShell, self).init_alias()
253
259
254 # Now define aliases that only make sense on the terminal, because they
260 # Now define aliases that only make sense on the terminal, because they
255 # need direct access to the console in a way that we can't emulate in
261 # need direct access to the console in a way that we can't emulate in
256 # GUI or web frontend
262 # GUI or web frontend
257 if os.name == 'posix':
263 if os.name == 'posix':
258 for cmd in ['clear', 'more', 'less', 'man']:
264 for cmd in ['clear', 'more', 'less', 'man']:
259 self.alias_manager.soft_define_alias(cmd, cmd)
265 self.alias_manager.soft_define_alias(cmd, cmd)
260
266
261
267
262 def __init__(self, *args, **kwargs):
268 def __init__(self, *args, **kwargs):
263 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
269 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
264 self.init_prompt_toolkit_cli()
270 self.init_prompt_toolkit_cli()
265 self.init_term_title()
271 self.init_term_title()
266 self.keep_running = True
272 self.keep_running = True
267
273
268 def ask_exit(self):
274 def ask_exit(self):
269 self.keep_running = False
275 self.keep_running = False
270
276
271 rl_next_input = None
277 rl_next_input = None
272
278
273 def pre_prompt(self):
279 def pre_prompt(self):
274 if self.rl_next_input:
280 if self.rl_next_input:
275 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
281 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
276 self.rl_next_input = None
282 self.rl_next_input = None
277
283
278 def interact(self):
284 def interact(self):
279 while self.keep_running:
285 while self.keep_running:
280 print(self.separate_in, end='')
286 print(self.separate_in, end='')
281
287
282 try:
288 try:
283 code = self.prompt_for_code()
289 code = self.prompt_for_code()
284 except EOFError:
290 except EOFError:
285 if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
291 if (not self.confirm_exit) \
292 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
286 self.ask_exit()
293 self.ask_exit()
287
294
288 else:
295 else:
289 if code:
296 if code:
290 self.run_cell(code, store_history=True)
297 self.run_cell(code, store_history=True)
291
298
292 def mainloop(self):
299 def mainloop(self):
293 # An extra layer of protection in case someone mashing Ctrl-C breaks
300 # An extra layer of protection in case someone mashing Ctrl-C breaks
294 # out of our internal code.
301 # out of our internal code.
295 while True:
302 while True:
296 try:
303 try:
297 self.interact()
304 self.interact()
298 break
305 break
299 except KeyboardInterrupt:
306 except KeyboardInterrupt:
300 print("\nKeyboardInterrupt escaped interact()\n")
307 print("\nKeyboardInterrupt escaped interact()\n")
301
308
302 _inputhook = None
309 _inputhook = None
303 def inputhook(self, context):
310 def inputhook(self, context):
304 if self._inputhook is not None:
311 if self._inputhook is not None:
305 self._inputhook(context)
312 self._inputhook(context)
306
313
307 def enable_gui(self, gui=None):
314 def enable_gui(self, gui=None):
308 if gui:
315 if gui:
309 self._inputhook = get_inputhook_func(gui)
316 self._inputhook = get_inputhook_func(gui)
310 else:
317 else:
311 self._inputhook = None
318 self._inputhook = None
312
319
313 if __name__ == '__main__':
320 if __name__ == '__main__':
314 TerminalInteractiveShell.instance().interact()
321 TerminalInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now