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