##// END OF EJS Templates
Make behaviour more natural with blank lines at the end of input
Thomas Kluyver -
Show More
@@ -1,209 +1,211 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 sys
4 import sys
5
5
6 from IPython.core.interactiveshell import InteractiveShell
6 from IPython.core.interactiveshell import InteractiveShell
7 from IPython.utils.py3compat import PY3, cast_unicode_py2
7 from IPython.utils.py3compat import PY3, cast_unicode_py2
8 from traitlets import Bool, Unicode, Dict
8 from traitlets import Bool, Unicode, Dict
9
9
10 from prompt_toolkit.completion import Completer, Completion
10 from prompt_toolkit.completion import Completer, Completion
11 from prompt_toolkit.enums import DEFAULT_BUFFER
11 from prompt_toolkit.enums import DEFAULT_BUFFER
12 from prompt_toolkit.filters import HasFocus, HasSelection
12 from prompt_toolkit.filters import HasFocus, HasSelection
13 from prompt_toolkit.history import InMemoryHistory
13 from prompt_toolkit.history import InMemoryHistory
14 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop
14 from prompt_toolkit.shortcuts import create_prompt_application, create_eventloop
15 from prompt_toolkit.interface import CommandLineInterface
15 from prompt_toolkit.interface import CommandLineInterface
16 from prompt_toolkit.key_binding.manager import KeyBindingManager
16 from prompt_toolkit.key_binding.manager import KeyBindingManager
17 from prompt_toolkit.key_binding.vi_state import InputMode
17 from prompt_toolkit.key_binding.vi_state import InputMode
18 from prompt_toolkit.key_binding.bindings.vi import ViStateFilter
18 from prompt_toolkit.key_binding.bindings.vi import ViStateFilter
19 from prompt_toolkit.keys import Keys
19 from prompt_toolkit.keys import Keys
20 from prompt_toolkit.layout.lexers import PygmentsLexer
20 from prompt_toolkit.layout.lexers import PygmentsLexer
21 from prompt_toolkit.styles import PygmentsStyle
21 from prompt_toolkit.styles import PygmentsStyle
22
22
23 from pygments.styles import get_style_by_name
23 from pygments.styles import get_style_by_name
24 from pygments.lexers import Python3Lexer, PythonLexer
24 from pygments.lexers import Python3Lexer, PythonLexer
25 from pygments.token import Token
25 from pygments.token import Token
26
26
27 from .pt_inputhooks import get_inputhook_func
27 from .pt_inputhooks import get_inputhook_func
28 from .interactiveshell import get_default_editor
28 from .interactiveshell import get_default_editor
29
29
30
30
31 class IPythonPTCompleter(Completer):
31 class IPythonPTCompleter(Completer):
32 """Adaptor to provide IPython completions to prompt_toolkit"""
32 """Adaptor to provide IPython completions to prompt_toolkit"""
33 def __init__(self, ipy_completer):
33 def __init__(self, ipy_completer):
34 self.ipy_completer = ipy_completer
34 self.ipy_completer = ipy_completer
35
35
36 def get_completions(self, document, complete_event):
36 def get_completions(self, document, complete_event):
37 if not document.current_line.strip():
37 if not document.current_line.strip():
38 return
38 return
39
39
40 used, matches = self.ipy_completer.complete(
40 used, matches = self.ipy_completer.complete(
41 line_buffer=document.current_line,
41 line_buffer=document.current_line,
42 cursor_pos=document.cursor_position_col
42 cursor_pos=document.cursor_position_col
43 )
43 )
44 start_pos = -len(used)
44 start_pos = -len(used)
45 for m in matches:
45 for m in matches:
46 yield Completion(m, start_position=start_pos)
46 yield Completion(m, start_position=start_pos)
47
47
48 class PTInteractiveShell(InteractiveShell):
48 class PTInteractiveShell(InteractiveShell):
49 colors_force = True
49 colors_force = True
50
50
51 pt_cli = None
51 pt_cli = None
52
52
53 vi_mode = Bool(False, config=True,
53 vi_mode = Bool(False, config=True,
54 help="Use vi style keybindings at the prompt",
54 help="Use vi style keybindings at the prompt",
55 )
55 )
56
56
57 highlighting_style = Unicode('', config=True,
57 highlighting_style = Unicode('', config=True,
58 help="The name of a Pygments style to use for syntax highlighting"
58 help="The name of a Pygments style to use for syntax highlighting"
59 )
59 )
60
60
61 highlighting_style_overrides = Dict(config=True,
61 highlighting_style_overrides = Dict(config=True,
62 help="Override highlighting format for specific tokens"
62 help="Override highlighting format for specific tokens"
63 )
63 )
64
64
65 editor = Unicode(get_default_editor(), config=True,
65 editor = Unicode(get_default_editor(), config=True,
66 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
66 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
67 )
67 )
68
68
69 def get_prompt_tokens(self, cli):
69 def get_prompt_tokens(self, cli):
70 return [
70 return [
71 (Token.Prompt, 'In ['),
71 (Token.Prompt, 'In ['),
72 (Token.PromptNum, str(self.execution_count)),
72 (Token.PromptNum, str(self.execution_count)),
73 (Token.Prompt, ']: '),
73 (Token.Prompt, ']: '),
74 ]
74 ]
75
75
76 def get_continuation_tokens(self, cli, width):
76 def get_continuation_tokens(self, cli, width):
77 return [
77 return [
78 (Token.Prompt, (' ' * (width - 2)) + ': '),
78 (Token.Prompt, (' ' * (width - 2)) + ': '),
79 ]
79 ]
80
80
81 def init_prompt_toolkit_cli(self):
81 def init_prompt_toolkit_cli(self):
82 kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode)
82 kbmanager = KeyBindingManager.for_prompt(enable_vi_mode=self.vi_mode)
83 insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT)
83 insert_mode = ViStateFilter(kbmanager.get_vi_state, InputMode.INSERT)
84 # Ctrl+J == Enter, seemingly
84 # Ctrl+J == Enter, seemingly
85 @kbmanager.registry.add_binding(Keys.ControlJ,
85 @kbmanager.registry.add_binding(Keys.ControlJ,
86 filter=(HasFocus(DEFAULT_BUFFER)
86 filter=(HasFocus(DEFAULT_BUFFER)
87 & ~HasSelection()
87 & ~HasSelection()
88 & insert_mode
88 & insert_mode
89 ))
89 ))
90 def _(event):
90 def _(event):
91 b = event.current_buffer
91 b = event.current_buffer
92 if not b.document.on_last_line:
92 d = b.document
93 if not (d.on_last_line or d.cursor_position_row >= d.line_count
94 - d.empty_line_count_at_the_end()):
93 b.newline()
95 b.newline()
94 return
96 return
95
97
96 status, indent = self.input_splitter.check_complete(b.document.text)
98 status, indent = self.input_splitter.check_complete(d.text)
97
99
98 if (status != 'incomplete') and b.accept_action.is_returnable:
100 if (status != 'incomplete') and b.accept_action.is_returnable:
99 b.accept_action.validate_and_handle(event.cli, b)
101 b.accept_action.validate_and_handle(event.cli, b)
100 else:
102 else:
101 b.insert_text('\n' + (' ' * (indent or 0)))
103 b.insert_text('\n' + (' ' * (indent or 0)))
102
104
103 @kbmanager.registry.add_binding(Keys.ControlC)
105 @kbmanager.registry.add_binding(Keys.ControlC)
104 def _(event):
106 def _(event):
105 event.current_buffer.reset()
107 event.current_buffer.reset()
106
108
107 # Pre-populate history from IPython's history database
109 # Pre-populate history from IPython's history database
108 history = InMemoryHistory()
110 history = InMemoryHistory()
109 last_cell = u""
111 last_cell = u""
110 for _, _, cell in self.history_manager.get_tail(self.history_load_length,
112 for _, _, cell in self.history_manager.get_tail(self.history_load_length,
111 include_latest=True):
113 include_latest=True):
112 # Ignore blank lines and consecutive duplicates
114 # Ignore blank lines and consecutive duplicates
113 cell = cell.rstrip()
115 cell = cell.rstrip()
114 if cell and (cell != last_cell):
116 if cell and (cell != last_cell):
115 history.append(cell)
117 history.append(cell)
116
118
117 style_overrides = {
119 style_overrides = {
118 Token.Prompt: '#009900',
120 Token.Prompt: '#009900',
119 Token.PromptNum: '#00ff00 bold',
121 Token.PromptNum: '#00ff00 bold',
120 }
122 }
121 if self.highlighting_style:
123 if self.highlighting_style:
122 style_cls = get_style_by_name(self.highlighting_style)
124 style_cls = get_style_by_name(self.highlighting_style)
123 else:
125 else:
124 style_cls = get_style_by_name('default')
126 style_cls = get_style_by_name('default')
125 # The default theme needs to be visible on both a dark background
127 # The default theme needs to be visible on both a dark background
126 # and a light background, because we can't tell what the terminal
128 # and a light background, because we can't tell what the terminal
127 # looks like. These tweaks to the default theme help with that.
129 # looks like. These tweaks to the default theme help with that.
128 style_overrides.update({
130 style_overrides.update({
129 Token.Number: '#007700',
131 Token.Number: '#007700',
130 Token.Operator: 'noinherit',
132 Token.Operator: 'noinherit',
131 Token.String: '#BB6622',
133 Token.String: '#BB6622',
132 Token.Name.Function: '#2080D0',
134 Token.Name.Function: '#2080D0',
133 Token.Name.Class: 'bold #2080D0',
135 Token.Name.Class: 'bold #2080D0',
134 Token.Name.Namespace: 'bold #2080D0',
136 Token.Name.Namespace: 'bold #2080D0',
135 })
137 })
136 style_overrides.update(self.highlighting_style_overrides)
138 style_overrides.update(self.highlighting_style_overrides)
137 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
139 style = PygmentsStyle.from_defaults(pygments_style_cls=style_cls,
138 style_dict=style_overrides)
140 style_dict=style_overrides)
139
141
140 app = create_prompt_application(multiline=True,
142 app = create_prompt_application(multiline=True,
141 lexer=PygmentsLexer(Python3Lexer if PY3 else PythonLexer),
143 lexer=PygmentsLexer(Python3Lexer if PY3 else PythonLexer),
142 get_prompt_tokens=self.get_prompt_tokens,
144 get_prompt_tokens=self.get_prompt_tokens,
143 get_continuation_tokens=self.get_continuation_tokens,
145 get_continuation_tokens=self.get_continuation_tokens,
144 key_bindings_registry=kbmanager.registry,
146 key_bindings_registry=kbmanager.registry,
145 history=history,
147 history=history,
146 completer=IPythonPTCompleter(self.Completer),
148 completer=IPythonPTCompleter(self.Completer),
147 enable_history_search=True,
149 enable_history_search=True,
148 style=style,
150 style=style,
149 )
151 )
150
152
151 self.pt_cli = CommandLineInterface(app,
153 self.pt_cli = CommandLineInterface(app,
152 eventloop=create_eventloop(self.inputhook))
154 eventloop=create_eventloop(self.inputhook))
153
155
154 def init_io(self):
156 def init_io(self):
155 if sys.platform not in {'win32', 'cli'}:
157 if sys.platform not in {'win32', 'cli'}:
156 return
158 return
157
159
158 import colorama
160 import colorama
159 colorama.init()
161 colorama.init()
160
162
161 # For some reason we make these wrappers around stdout/stderr.
163 # For some reason we make these wrappers around stdout/stderr.
162 # For now, we need to reset them so all output gets coloured.
164 # For now, we need to reset them so all output gets coloured.
163 # https://github.com/ipython/ipython/issues/8669
165 # https://github.com/ipython/ipython/issues/8669
164 from IPython.utils import io
166 from IPython.utils import io
165 io.stdout = io.IOStream(sys.stdout)
167 io.stdout = io.IOStream(sys.stdout)
166 io.stderr = io.IOStream(sys.stderr)
168 io.stderr = io.IOStream(sys.stderr)
167
169
168 def __init__(self, *args, **kwargs):
170 def __init__(self, *args, **kwargs):
169 super(PTInteractiveShell, self).__init__(*args, **kwargs)
171 super(PTInteractiveShell, self).__init__(*args, **kwargs)
170 self.init_prompt_toolkit_cli()
172 self.init_prompt_toolkit_cli()
171 self.keep_running = True
173 self.keep_running = True
172
174
173 def ask_exit(self):
175 def ask_exit(self):
174 self.keep_running = False
176 self.keep_running = False
175
177
176 rl_next_input = None
178 rl_next_input = None
177
179
178 def pre_prompt(self):
180 def pre_prompt(self):
179 if self.rl_next_input:
181 if self.rl_next_input:
180 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
182 self.pt_cli.application.buffer.text = cast_unicode_py2(self.rl_next_input)
181 self.rl_next_input = None
183 self.rl_next_input = None
182
184
183 def interact(self):
185 def interact(self):
184 while self.keep_running:
186 while self.keep_running:
185 print(self.separate_in, end='')
187 print(self.separate_in, end='')
186
188
187 try:
189 try:
188 document = self.pt_cli.run(pre_run=self.pre_prompt)
190 document = self.pt_cli.run(pre_run=self.pre_prompt)
189 except EOFError:
191 except EOFError:
190 if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
192 if self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
191 self.ask_exit()
193 self.ask_exit()
192
194
193 else:
195 else:
194 if document:
196 if document:
195 self.run_cell(document.text, store_history=True)
197 self.run_cell(document.text, store_history=True)
196
198
197 _inputhook = None
199 _inputhook = None
198 def inputhook(self, context):
200 def inputhook(self, context):
199 if self._inputhook is not None:
201 if self._inputhook is not None:
200 self._inputhook(context)
202 self._inputhook(context)
201
203
202 def enable_gui(self, gui=None):
204 def enable_gui(self, gui=None):
203 if gui:
205 if gui:
204 self._inputhook = get_inputhook_func(gui)
206 self._inputhook = get_inputhook_func(gui)
205 else:
207 else:
206 self._inputhook = None
208 self._inputhook = None
207
209
208 if __name__ == '__main__':
210 if __name__ == '__main__':
209 PTInteractiveShell.instance().interact()
211 PTInteractiveShell.instance().interact()
General Comments 0
You need to be logged in to leave comments. Login now