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