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