##// END OF EJS Templates
Improve 'save history' menu (%save)...
Matthias BUSSONNIER -
Show More
@@ -1,636 +1,638 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from IPython.external.qt import QtCore, QtGui
10 from IPython.external.qt import QtCore, QtGui
11
11
12 # Local imports
12 # Local imports
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 from IPython.core.oinspect import call_tip
14 from IPython.core.oinspect import call_tip
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 from IPython.utils.traitlets import Bool, Instance, Unicode
16 from IPython.utils.traitlets import Bool, Instance, Unicode
17 from bracket_matcher import BracketMatcher
17 from bracket_matcher import BracketMatcher
18 from call_tip_widget import CallTipWidget
18 from call_tip_widget import CallTipWidget
19 from completion_lexer import CompletionLexer
19 from completion_lexer import CompletionLexer
20 from history_console_widget import HistoryConsoleWidget
20 from history_console_widget import HistoryConsoleWidget
21 from pygments_highlighter import PygmentsHighlighter
21 from pygments_highlighter import PygmentsHighlighter
22
22
23
23
24 class FrontendHighlighter(PygmentsHighlighter):
24 class FrontendHighlighter(PygmentsHighlighter):
25 """ A PygmentsHighlighter that understands and ignores prompts.
25 """ A PygmentsHighlighter that understands and ignores prompts.
26 """
26 """
27
27
28 def __init__(self, frontend):
28 def __init__(self, frontend):
29 super(FrontendHighlighter, self).__init__(frontend._control.document())
29 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 self._current_offset = 0
30 self._current_offset = 0
31 self._frontend = frontend
31 self._frontend = frontend
32 self.highlighting_on = False
32 self.highlighting_on = False
33
33
34 def highlightBlock(self, string):
34 def highlightBlock(self, string):
35 """ Highlight a block of text. Reimplemented to highlight selectively.
35 """ Highlight a block of text. Reimplemented to highlight selectively.
36 """
36 """
37 if not self.highlighting_on:
37 if not self.highlighting_on:
38 return
38 return
39
39
40 # The input to this function is a unicode string that may contain
40 # The input to this function is a unicode string that may contain
41 # paragraph break characters, non-breaking spaces, etc. Here we acquire
41 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 # the string as plain text so we can compare it.
42 # the string as plain text so we can compare it.
43 current_block = self.currentBlock()
43 current_block = self.currentBlock()
44 string = self._frontend._get_block_plain_text(current_block)
44 string = self._frontend._get_block_plain_text(current_block)
45
45
46 # Decide whether to check for the regular or continuation prompt.
46 # Decide whether to check for the regular or continuation prompt.
47 if current_block.contains(self._frontend._prompt_pos):
47 if current_block.contains(self._frontend._prompt_pos):
48 prompt = self._frontend._prompt
48 prompt = self._frontend._prompt
49 else:
49 else:
50 prompt = self._frontend._continuation_prompt
50 prompt = self._frontend._continuation_prompt
51
51
52 # Only highlight if we can identify a prompt, but make sure not to
52 # Only highlight if we can identify a prompt, but make sure not to
53 # highlight the prompt.
53 # highlight the prompt.
54 if string.startswith(prompt):
54 if string.startswith(prompt):
55 self._current_offset = len(prompt)
55 self._current_offset = len(prompt)
56 string = string[len(prompt):]
56 string = string[len(prompt):]
57 super(FrontendHighlighter, self).highlightBlock(string)
57 super(FrontendHighlighter, self).highlightBlock(string)
58
58
59 def rehighlightBlock(self, block):
59 def rehighlightBlock(self, block):
60 """ Reimplemented to temporarily enable highlighting if disabled.
60 """ Reimplemented to temporarily enable highlighting if disabled.
61 """
61 """
62 old = self.highlighting_on
62 old = self.highlighting_on
63 self.highlighting_on = True
63 self.highlighting_on = True
64 super(FrontendHighlighter, self).rehighlightBlock(block)
64 super(FrontendHighlighter, self).rehighlightBlock(block)
65 self.highlighting_on = old
65 self.highlighting_on = old
66
66
67 def setFormat(self, start, count, format):
67 def setFormat(self, start, count, format):
68 """ Reimplemented to highlight selectively.
68 """ Reimplemented to highlight selectively.
69 """
69 """
70 start += self._current_offset
70 start += self._current_offset
71 super(FrontendHighlighter, self).setFormat(start, count, format)
71 super(FrontendHighlighter, self).setFormat(start, count, format)
72
72
73
73
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
75 """ A Qt frontend for a generic Python kernel.
75 """ A Qt frontend for a generic Python kernel.
76 """
76 """
77
77
78 # The text to show when the kernel is (re)started.
78 # The text to show when the kernel is (re)started.
79 banner = Unicode()
79 banner = Unicode()
80
80
81 # An option and corresponding signal for overriding the default kernel
81 # An option and corresponding signal for overriding the default kernel
82 # interrupt behavior.
82 # interrupt behavior.
83 custom_interrupt = Bool(False)
83 custom_interrupt = Bool(False)
84 custom_interrupt_requested = QtCore.Signal()
84 custom_interrupt_requested = QtCore.Signal()
85
85
86 # An option and corresponding signals for overriding the default kernel
86 # An option and corresponding signals for overriding the default kernel
87 # restart behavior.
87 # restart behavior.
88 custom_restart = Bool(False)
88 custom_restart = Bool(False)
89 custom_restart_kernel_died = QtCore.Signal(float)
89 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_requested = QtCore.Signal()
90 custom_restart_requested = QtCore.Signal()
91
91
92 # Whether to automatically show calltips on open-parentheses.
92 # Whether to automatically show calltips on open-parentheses.
93 enable_calltips = Bool(True, config=True,
93 enable_calltips = Bool(True, config=True,
94 help="Whether to draw information calltips on open-parentheses.")
94 help="Whether to draw information calltips on open-parentheses.")
95
95
96 # Emitted when a user visible 'execute_request' has been submitted to the
96 # Emitted when a user visible 'execute_request' has been submitted to the
97 # kernel from the FrontendWidget. Contains the code to be executed.
97 # kernel from the FrontendWidget. Contains the code to be executed.
98 executing = QtCore.Signal(object)
98 executing = QtCore.Signal(object)
99
99
100 # Emitted when a user-visible 'execute_reply' has been received from the
100 # Emitted when a user-visible 'execute_reply' has been received from the
101 # kernel and processed by the FrontendWidget. Contains the response message.
101 # kernel and processed by the FrontendWidget. Contains the response message.
102 executed = QtCore.Signal(object)
102 executed = QtCore.Signal(object)
103
103
104 # Emitted when an exit request has been received from the kernel.
104 # Emitted when an exit request has been received from the kernel.
105 exit_requested = QtCore.Signal()
105 exit_requested = QtCore.Signal()
106
106
107 # Protected class variables.
107 # Protected class variables.
108 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
108 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
109 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
109 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
110 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
110 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
111 _input_splitter_class = InputSplitter
111 _input_splitter_class = InputSplitter
112 _local_kernel = False
112 _local_kernel = False
113 _highlighter = Instance(FrontendHighlighter)
113 _highlighter = Instance(FrontendHighlighter)
114
114
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116 # 'object' interface
116 # 'object' interface
117 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
118
118
119 def __init__(self, *args, **kw):
119 def __init__(self, *args, **kw):
120 super(FrontendWidget, self).__init__(*args, **kw)
120 super(FrontendWidget, self).__init__(*args, **kw)
121
121
122 # FrontendWidget protected variables.
122 # FrontendWidget protected variables.
123 self._bracket_matcher = BracketMatcher(self._control)
123 self._bracket_matcher = BracketMatcher(self._control)
124 self._call_tip_widget = CallTipWidget(self._control)
124 self._call_tip_widget = CallTipWidget(self._control)
125 self._completion_lexer = CompletionLexer(PythonLexer())
125 self._completion_lexer = CompletionLexer(PythonLexer())
126 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
126 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
127 self._hidden = False
127 self._hidden = False
128 self._highlighter = FrontendHighlighter(self)
128 self._highlighter = FrontendHighlighter(self)
129 self._input_splitter = self._input_splitter_class(input_mode='cell')
129 self._input_splitter = self._input_splitter_class(input_mode='cell')
130 self._kernel_manager = None
130 self._kernel_manager = None
131 self._request_info = {}
131 self._request_info = {}
132
132
133 # Configure the ConsoleWidget.
133 # Configure the ConsoleWidget.
134 self.tab_width = 4
134 self.tab_width = 4
135 self._set_continuation_prompt('... ')
135 self._set_continuation_prompt('... ')
136
136
137 # Configure the CallTipWidget.
137 # Configure the CallTipWidget.
138 self._call_tip_widget.setFont(self.font)
138 self._call_tip_widget.setFont(self.font)
139 self.font_changed.connect(self._call_tip_widget.setFont)
139 self.font_changed.connect(self._call_tip_widget.setFont)
140
140
141 # Configure actions.
141 # Configure actions.
142 action = self._copy_raw_action
142 action = self._copy_raw_action
143 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
143 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
144 action.setEnabled(False)
144 action.setEnabled(False)
145 action.setShortcut(QtGui.QKeySequence(key))
145 action.setShortcut(QtGui.QKeySequence(key))
146 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
146 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
147 action.triggered.connect(self.copy_raw)
147 action.triggered.connect(self.copy_raw)
148 self.copy_available.connect(action.setEnabled)
148 self.copy_available.connect(action.setEnabled)
149 self.addAction(action)
149 self.addAction(action)
150
150
151 # Connect signal handlers.
151 # Connect signal handlers.
152 document = self._control.document()
152 document = self._control.document()
153 document.contentsChange.connect(self._document_contents_change)
153 document.contentsChange.connect(self._document_contents_change)
154
154
155 # Set flag for whether we are connected via localhost.
155 # Set flag for whether we are connected via localhost.
156 self._local_kernel = kw.get('local_kernel',
156 self._local_kernel = kw.get('local_kernel',
157 FrontendWidget._local_kernel)
157 FrontendWidget._local_kernel)
158
158
159 #---------------------------------------------------------------------------
159 #---------------------------------------------------------------------------
160 # 'ConsoleWidget' public interface
160 # 'ConsoleWidget' public interface
161 #---------------------------------------------------------------------------
161 #---------------------------------------------------------------------------
162
162
163 def copy(self):
163 def copy(self):
164 """ Copy the currently selected text to the clipboard, removing prompts.
164 """ Copy the currently selected text to the clipboard, removing prompts.
165 """
165 """
166 text = self._control.textCursor().selection().toPlainText()
166 text = self._control.textCursor().selection().toPlainText()
167 if text:
167 if text:
168 lines = map(transform_classic_prompt, text.splitlines())
168 lines = map(transform_classic_prompt, text.splitlines())
169 text = '\n'.join(lines)
169 text = '\n'.join(lines)
170 QtGui.QApplication.clipboard().setText(text)
170 QtGui.QApplication.clipboard().setText(text)
171
171
172 #---------------------------------------------------------------------------
172 #---------------------------------------------------------------------------
173 # 'ConsoleWidget' abstract interface
173 # 'ConsoleWidget' abstract interface
174 #---------------------------------------------------------------------------
174 #---------------------------------------------------------------------------
175
175
176 def _is_complete(self, source, interactive):
176 def _is_complete(self, source, interactive):
177 """ Returns whether 'source' can be completely processed and a new
177 """ Returns whether 'source' can be completely processed and a new
178 prompt created. When triggered by an Enter/Return key press,
178 prompt created. When triggered by an Enter/Return key press,
179 'interactive' is True; otherwise, it is False.
179 'interactive' is True; otherwise, it is False.
180 """
180 """
181 complete = self._input_splitter.push(source)
181 complete = self._input_splitter.push(source)
182 if interactive:
182 if interactive:
183 complete = not self._input_splitter.push_accepts_more()
183 complete = not self._input_splitter.push_accepts_more()
184 return complete
184 return complete
185
185
186 def _execute(self, source, hidden):
186 def _execute(self, source, hidden):
187 """ Execute 'source'. If 'hidden', do not show any output.
187 """ Execute 'source'. If 'hidden', do not show any output.
188
188
189 See parent class :meth:`execute` docstring for full details.
189 See parent class :meth:`execute` docstring for full details.
190 """
190 """
191 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
191 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
192 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
192 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
193 self._hidden = hidden
193 self._hidden = hidden
194 if not hidden:
194 if not hidden:
195 self.executing.emit(source)
195 self.executing.emit(source)
196
196
197 def _prompt_started_hook(self):
197 def _prompt_started_hook(self):
198 """ Called immediately after a new prompt is displayed.
198 """ Called immediately after a new prompt is displayed.
199 """
199 """
200 if not self._reading:
200 if not self._reading:
201 self._highlighter.highlighting_on = True
201 self._highlighter.highlighting_on = True
202
202
203 def _prompt_finished_hook(self):
203 def _prompt_finished_hook(self):
204 """ Called immediately after a prompt is finished, i.e. when some input
204 """ Called immediately after a prompt is finished, i.e. when some input
205 will be processed and a new prompt displayed.
205 will be processed and a new prompt displayed.
206 """
206 """
207 # Flush all state from the input splitter so the next round of
207 # Flush all state from the input splitter so the next round of
208 # reading input starts with a clean buffer.
208 # reading input starts with a clean buffer.
209 self._input_splitter.reset()
209 self._input_splitter.reset()
210
210
211 if not self._reading:
211 if not self._reading:
212 self._highlighter.highlighting_on = False
212 self._highlighter.highlighting_on = False
213
213
214 def _tab_pressed(self):
214 def _tab_pressed(self):
215 """ Called when the tab key is pressed. Returns whether to continue
215 """ Called when the tab key is pressed. Returns whether to continue
216 processing the event.
216 processing the event.
217 """
217 """
218 # Perform tab completion if:
218 # Perform tab completion if:
219 # 1) The cursor is in the input buffer.
219 # 1) The cursor is in the input buffer.
220 # 2) There is a non-whitespace character before the cursor.
220 # 2) There is a non-whitespace character before the cursor.
221 text = self._get_input_buffer_cursor_line()
221 text = self._get_input_buffer_cursor_line()
222 if text is None:
222 if text is None:
223 return False
223 return False
224 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
224 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
225 if complete:
225 if complete:
226 self._complete()
226 self._complete()
227 return not complete
227 return not complete
228
228
229 #---------------------------------------------------------------------------
229 #---------------------------------------------------------------------------
230 # 'ConsoleWidget' protected interface
230 # 'ConsoleWidget' protected interface
231 #---------------------------------------------------------------------------
231 #---------------------------------------------------------------------------
232
232
233 def _context_menu_make(self, pos):
233 def _context_menu_make(self, pos):
234 """ Reimplemented to add an action for raw copy.
234 """ Reimplemented to add an action for raw copy.
235 """
235 """
236 menu = super(FrontendWidget, self)._context_menu_make(pos)
236 menu = super(FrontendWidget, self)._context_menu_make(pos)
237 for before_action in menu.actions():
237 for before_action in menu.actions():
238 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
238 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
239 QtGui.QKeySequence.ExactMatch:
239 QtGui.QKeySequence.ExactMatch:
240 menu.insertAction(before_action, self._copy_raw_action)
240 menu.insertAction(before_action, self._copy_raw_action)
241 break
241 break
242 return menu
242 return menu
243
243
244 def _event_filter_console_keypress(self, event):
244 def _event_filter_console_keypress(self, event):
245 """ Reimplemented for execution interruption and smart backspace.
245 """ Reimplemented for execution interruption and smart backspace.
246 """
246 """
247 key = event.key()
247 key = event.key()
248 if self._control_key_down(event.modifiers(), include_command=False):
248 if self._control_key_down(event.modifiers(), include_command=False):
249
249
250 if key == QtCore.Qt.Key_C and self._executing:
250 if key == QtCore.Qt.Key_C and self._executing:
251 self.interrupt_kernel()
251 self.interrupt_kernel()
252 return True
252 return True
253
253
254 elif key == QtCore.Qt.Key_Period:
254 elif key == QtCore.Qt.Key_Period:
255 message = 'Are you sure you want to restart the kernel?'
255 message = 'Are you sure you want to restart the kernel?'
256 self.restart_kernel(message, now=False)
256 self.restart_kernel(message, now=False)
257 return True
257 return True
258
258
259 elif not event.modifiers() & QtCore.Qt.AltModifier:
259 elif not event.modifiers() & QtCore.Qt.AltModifier:
260
260
261 # Smart backspace: remove four characters in one backspace if:
261 # Smart backspace: remove four characters in one backspace if:
262 # 1) everything left of the cursor is whitespace
262 # 1) everything left of the cursor is whitespace
263 # 2) the four characters immediately left of the cursor are spaces
263 # 2) the four characters immediately left of the cursor are spaces
264 if key == QtCore.Qt.Key_Backspace:
264 if key == QtCore.Qt.Key_Backspace:
265 col = self._get_input_buffer_cursor_column()
265 col = self._get_input_buffer_cursor_column()
266 cursor = self._control.textCursor()
266 cursor = self._control.textCursor()
267 if col > 3 and not cursor.hasSelection():
267 if col > 3 and not cursor.hasSelection():
268 text = self._get_input_buffer_cursor_line()[:col]
268 text = self._get_input_buffer_cursor_line()[:col]
269 if text.endswith(' ') and not text.strip():
269 if text.endswith(' ') and not text.strip():
270 cursor.movePosition(QtGui.QTextCursor.Left,
270 cursor.movePosition(QtGui.QTextCursor.Left,
271 QtGui.QTextCursor.KeepAnchor, 4)
271 QtGui.QTextCursor.KeepAnchor, 4)
272 cursor.removeSelectedText()
272 cursor.removeSelectedText()
273 return True
273 return True
274
274
275 return super(FrontendWidget, self)._event_filter_console_keypress(event)
275 return super(FrontendWidget, self)._event_filter_console_keypress(event)
276
276
277 def _insert_continuation_prompt(self, cursor):
277 def _insert_continuation_prompt(self, cursor):
278 """ Reimplemented for auto-indentation.
278 """ Reimplemented for auto-indentation.
279 """
279 """
280 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
280 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
281 cursor.insertText(' ' * self._input_splitter.indent_spaces)
281 cursor.insertText(' ' * self._input_splitter.indent_spaces)
282
282
283 #---------------------------------------------------------------------------
283 #---------------------------------------------------------------------------
284 # 'BaseFrontendMixin' abstract interface
284 # 'BaseFrontendMixin' abstract interface
285 #---------------------------------------------------------------------------
285 #---------------------------------------------------------------------------
286
286
287 def _handle_complete_reply(self, rep):
287 def _handle_complete_reply(self, rep):
288 """ Handle replies for tab completion.
288 """ Handle replies for tab completion.
289 """
289 """
290 self.log.debug("complete: %s", rep.get('content', ''))
290 self.log.debug("complete: %s", rep.get('content', ''))
291 cursor = self._get_cursor()
291 cursor = self._get_cursor()
292 info = self._request_info.get('complete')
292 info = self._request_info.get('complete')
293 if info and info.id == rep['parent_header']['msg_id'] and \
293 if info and info.id == rep['parent_header']['msg_id'] and \
294 info.pos == cursor.position():
294 info.pos == cursor.position():
295 text = '.'.join(self._get_context())
295 text = '.'.join(self._get_context())
296 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
296 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
297 self._complete_with_items(cursor, rep['content']['matches'])
297 self._complete_with_items(cursor, rep['content']['matches'])
298
298
299 def _handle_execute_reply(self, msg):
299 def _handle_execute_reply(self, msg):
300 """ Handles replies for code execution.
300 """ Handles replies for code execution.
301 """
301 """
302 self.log.debug("execute: %s", msg.get('content', ''))
302 self.log.debug("execute: %s", msg.get('content', ''))
303 info = self._request_info.get('execute')
303 info = self._request_info.get('execute')
304 if info and info.id == msg['parent_header']['msg_id'] and \
304 if info and info.id == msg['parent_header']['msg_id'] and \
305 info.kind == 'user' and not self._hidden:
305 info.kind == 'user' and not self._hidden:
306 # Make sure that all output from the SUB channel has been processed
306 # Make sure that all output from the SUB channel has been processed
307 # before writing a new prompt.
307 # before writing a new prompt.
308 self.kernel_manager.sub_channel.flush()
308 self.kernel_manager.sub_channel.flush()
309
309
310 # Reset the ANSI style information to prevent bad text in stdout
310 # Reset the ANSI style information to prevent bad text in stdout
311 # from messing up our colors. We're not a true terminal so we're
311 # from messing up our colors. We're not a true terminal so we're
312 # allowed to do this.
312 # allowed to do this.
313 if self.ansi_codes:
313 if self.ansi_codes:
314 self._ansi_processor.reset_sgr()
314 self._ansi_processor.reset_sgr()
315
315
316 content = msg['content']
316 content = msg['content']
317 status = content['status']
317 status = content['status']
318 if status == 'ok':
318 if status == 'ok':
319 self._process_execute_ok(msg)
319 self._process_execute_ok(msg)
320 elif status == 'error':
320 elif status == 'error':
321 self._process_execute_error(msg)
321 self._process_execute_error(msg)
322 elif status == 'abort':
322 elif status == 'abort':
323 self._process_execute_abort(msg)
323 self._process_execute_abort(msg)
324
324
325 self._show_interpreter_prompt_for_reply(msg)
325 self._show_interpreter_prompt_for_reply(msg)
326 self.executed.emit(msg)
326 self.executed.emit(msg)
327 else:
328 super(FrontendWidget, self)._handle_execute_reply(msg)
327
329
328 def _handle_input_request(self, msg):
330 def _handle_input_request(self, msg):
329 """ Handle requests for raw_input.
331 """ Handle requests for raw_input.
330 """
332 """
331 self.log.debug("input: %s", msg.get('content', ''))
333 self.log.debug("input: %s", msg.get('content', ''))
332 if self._hidden:
334 if self._hidden:
333 raise RuntimeError('Request for raw input during hidden execution.')
335 raise RuntimeError('Request for raw input during hidden execution.')
334
336
335 # Make sure that all output from the SUB channel has been processed
337 # Make sure that all output from the SUB channel has been processed
336 # before entering readline mode.
338 # before entering readline mode.
337 self.kernel_manager.sub_channel.flush()
339 self.kernel_manager.sub_channel.flush()
338
340
339 def callback(line):
341 def callback(line):
340 self.kernel_manager.stdin_channel.input(line)
342 self.kernel_manager.stdin_channel.input(line)
341 self._readline(msg['content']['prompt'], callback=callback)
343 self._readline(msg['content']['prompt'], callback=callback)
342
344
343 def _handle_kernel_died(self, since_last_heartbeat):
345 def _handle_kernel_died(self, since_last_heartbeat):
344 """ Handle the kernel's death by asking if the user wants to restart.
346 """ Handle the kernel's death by asking if the user wants to restart.
345 """
347 """
346 self.log.debug("kernel died: %s", since_last_heartbeat)
348 self.log.debug("kernel died: %s", since_last_heartbeat)
347 if self.custom_restart:
349 if self.custom_restart:
348 self.custom_restart_kernel_died.emit(since_last_heartbeat)
350 self.custom_restart_kernel_died.emit(since_last_heartbeat)
349 else:
351 else:
350 message = 'The kernel heartbeat has been inactive for %.2f ' \
352 message = 'The kernel heartbeat has been inactive for %.2f ' \
351 'seconds. Do you want to restart the kernel? You may ' \
353 'seconds. Do you want to restart the kernel? You may ' \
352 'first want to check the network connection.' % \
354 'first want to check the network connection.' % \
353 since_last_heartbeat
355 since_last_heartbeat
354 self.restart_kernel(message, now=True)
356 self.restart_kernel(message, now=True)
355
357
356 def _handle_object_info_reply(self, rep):
358 def _handle_object_info_reply(self, rep):
357 """ Handle replies for call tips.
359 """ Handle replies for call tips.
358 """
360 """
359 self.log.debug("oinfo: %s", rep.get('content', ''))
361 self.log.debug("oinfo: %s", rep.get('content', ''))
360 cursor = self._get_cursor()
362 cursor = self._get_cursor()
361 info = self._request_info.get('call_tip')
363 info = self._request_info.get('call_tip')
362 if info and info.id == rep['parent_header']['msg_id'] and \
364 if info and info.id == rep['parent_header']['msg_id'] and \
363 info.pos == cursor.position():
365 info.pos == cursor.position():
364 # Get the information for a call tip. For now we format the call
366 # Get the information for a call tip. For now we format the call
365 # line as string, later we can pass False to format_call and
367 # line as string, later we can pass False to format_call and
366 # syntax-highlight it ourselves for nicer formatting in the
368 # syntax-highlight it ourselves for nicer formatting in the
367 # calltip.
369 # calltip.
368 content = rep['content']
370 content = rep['content']
369 # if this is from pykernel, 'docstring' will be the only key
371 # if this is from pykernel, 'docstring' will be the only key
370 if content.get('ismagic', False):
372 if content.get('ismagic', False):
371 # Don't generate a call-tip for magics. Ideally, we should
373 # Don't generate a call-tip for magics. Ideally, we should
372 # generate a tooltip, but not on ( like we do for actual
374 # generate a tooltip, but not on ( like we do for actual
373 # callables.
375 # callables.
374 call_info, doc = None, None
376 call_info, doc = None, None
375 else:
377 else:
376 call_info, doc = call_tip(content, format_call=True)
378 call_info, doc = call_tip(content, format_call=True)
377 if call_info or doc:
379 if call_info or doc:
378 self._call_tip_widget.show_call_info(call_info, doc)
380 self._call_tip_widget.show_call_info(call_info, doc)
379
381
380 def _handle_pyout(self, msg):
382 def _handle_pyout(self, msg):
381 """ Handle display hook output.
383 """ Handle display hook output.
382 """
384 """
383 self.log.debug("pyout: %s", msg.get('content', ''))
385 self.log.debug("pyout: %s", msg.get('content', ''))
384 if not self._hidden and self._is_from_this_session(msg):
386 if not self._hidden and self._is_from_this_session(msg):
385 text = msg['content']['data']
387 text = msg['content']['data']
386 self._append_plain_text(text + '\n', before_prompt=True)
388 self._append_plain_text(text + '\n', before_prompt=True)
387
389
388 def _handle_stream(self, msg):
390 def _handle_stream(self, msg):
389 """ Handle stdout, stderr, and stdin.
391 """ Handle stdout, stderr, and stdin.
390 """
392 """
391 self.log.debug("stream: %s", msg.get('content', ''))
393 self.log.debug("stream: %s", msg.get('content', ''))
392 if not self._hidden and self._is_from_this_session(msg):
394 if not self._hidden and self._is_from_this_session(msg):
393 # Most consoles treat tabs as being 8 space characters. Convert tabs
395 # Most consoles treat tabs as being 8 space characters. Convert tabs
394 # to spaces so that output looks as expected regardless of this
396 # to spaces so that output looks as expected regardless of this
395 # widget's tab width.
397 # widget's tab width.
396 text = msg['content']['data'].expandtabs(8)
398 text = msg['content']['data'].expandtabs(8)
397
399
398 self._append_plain_text(text, before_prompt=True)
400 self._append_plain_text(text, before_prompt=True)
399 self._control.moveCursor(QtGui.QTextCursor.End)
401 self._control.moveCursor(QtGui.QTextCursor.End)
400
402
401 def _handle_shutdown_reply(self, msg):
403 def _handle_shutdown_reply(self, msg):
402 """ Handle shutdown signal, only if from other console.
404 """ Handle shutdown signal, only if from other console.
403 """
405 """
404 self.log.debug("shutdown: %s", msg.get('content', ''))
406 self.log.debug("shutdown: %s", msg.get('content', ''))
405 if not self._hidden and not self._is_from_this_session(msg):
407 if not self._hidden and not self._is_from_this_session(msg):
406 if self._local_kernel:
408 if self._local_kernel:
407 if not msg['content']['restart']:
409 if not msg['content']['restart']:
408 sys.exit(0)
410 sys.exit(0)
409 else:
411 else:
410 # we just got notified of a restart!
412 # we just got notified of a restart!
411 time.sleep(0.25) # wait 1/4 sec to reset
413 time.sleep(0.25) # wait 1/4 sec to reset
412 # lest the request for a new prompt
414 # lest the request for a new prompt
413 # goes to the old kernel
415 # goes to the old kernel
414 self.reset()
416 self.reset()
415 else: # remote kernel, prompt on Kernel shutdown/reset
417 else: # remote kernel, prompt on Kernel shutdown/reset
416 title = self.window().windowTitle()
418 title = self.window().windowTitle()
417 if not msg['content']['restart']:
419 if not msg['content']['restart']:
418 reply = QtGui.QMessageBox.question(self, title,
420 reply = QtGui.QMessageBox.question(self, title,
419 "Kernel has been shutdown permanently. "
421 "Kernel has been shutdown permanently. "
420 "Close the Console?",
422 "Close the Console?",
421 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
423 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
422 if reply == QtGui.QMessageBox.Yes:
424 if reply == QtGui.QMessageBox.Yes:
423 sys.exit(0)
425 sys.exit(0)
424 else:
426 else:
425 reply = QtGui.QMessageBox.question(self, title,
427 reply = QtGui.QMessageBox.question(self, title,
426 "Kernel has been reset. Clear the Console?",
428 "Kernel has been reset. Clear the Console?",
427 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
429 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
428 if reply == QtGui.QMessageBox.Yes:
430 if reply == QtGui.QMessageBox.Yes:
429 time.sleep(0.25) # wait 1/4 sec to reset
431 time.sleep(0.25) # wait 1/4 sec to reset
430 # lest the request for a new prompt
432 # lest the request for a new prompt
431 # goes to the old kernel
433 # goes to the old kernel
432 self.reset()
434 self.reset()
433
435
434 def _started_channels(self):
436 def _started_channels(self):
435 """ Called when the KernelManager channels have started listening or
437 """ Called when the KernelManager channels have started listening or
436 when the frontend is assigned an already listening KernelManager.
438 when the frontend is assigned an already listening KernelManager.
437 """
439 """
438 self.reset()
440 self.reset()
439
441
440 #---------------------------------------------------------------------------
442 #---------------------------------------------------------------------------
441 # 'FrontendWidget' public interface
443 # 'FrontendWidget' public interface
442 #---------------------------------------------------------------------------
444 #---------------------------------------------------------------------------
443
445
444 def copy_raw(self):
446 def copy_raw(self):
445 """ Copy the currently selected text to the clipboard without attempting
447 """ Copy the currently selected text to the clipboard without attempting
446 to remove prompts or otherwise alter the text.
448 to remove prompts or otherwise alter the text.
447 """
449 """
448 self._control.copy()
450 self._control.copy()
449
451
450 def execute_file(self, path, hidden=False):
452 def execute_file(self, path, hidden=False):
451 """ Attempts to execute file with 'path'. If 'hidden', no output is
453 """ Attempts to execute file with 'path'. If 'hidden', no output is
452 shown.
454 shown.
453 """
455 """
454 self.execute('execfile(%r)' % path, hidden=hidden)
456 self.execute('execfile(%r)' % path, hidden=hidden)
455
457
456 def interrupt_kernel(self):
458 def interrupt_kernel(self):
457 """ Attempts to interrupt the running kernel.
459 """ Attempts to interrupt the running kernel.
458 """
460 """
459 if self.custom_interrupt:
461 if self.custom_interrupt:
460 self.custom_interrupt_requested.emit()
462 self.custom_interrupt_requested.emit()
461 elif self.kernel_manager.has_kernel:
463 elif self.kernel_manager.has_kernel:
462 self.kernel_manager.interrupt_kernel()
464 self.kernel_manager.interrupt_kernel()
463 else:
465 else:
464 self._append_plain_text('Kernel process is either remote or '
466 self._append_plain_text('Kernel process is either remote or '
465 'unspecified. Cannot interrupt.\n')
467 'unspecified. Cannot interrupt.\n')
466
468
467 def reset(self):
469 def reset(self):
468 """ Resets the widget to its initial state. Similar to ``clear``, but
470 """ Resets the widget to its initial state. Similar to ``clear``, but
469 also re-writes the banner and aborts execution if necessary.
471 also re-writes the banner and aborts execution if necessary.
470 """
472 """
471 if self._executing:
473 if self._executing:
472 self._executing = False
474 self._executing = False
473 self._request_info['execute'] = None
475 self._request_info['execute'] = None
474 self._reading = False
476 self._reading = False
475 self._highlighter.highlighting_on = False
477 self._highlighter.highlighting_on = False
476
478
477 self._control.clear()
479 self._control.clear()
478 self._append_plain_text(self.banner)
480 self._append_plain_text(self.banner)
479 self._show_interpreter_prompt()
481 self._show_interpreter_prompt()
480
482
481 def restart_kernel(self, message, now=False):
483 def restart_kernel(self, message, now=False):
482 """ Attempts to restart the running kernel.
484 """ Attempts to restart the running kernel.
483 """
485 """
484 # FIXME: now should be configurable via a checkbox in the dialog. Right
486 # FIXME: now should be configurable via a checkbox in the dialog. Right
485 # now at least the heartbeat path sets it to True and the manual restart
487 # now at least the heartbeat path sets it to True and the manual restart
486 # to False. But those should just be the pre-selected states of a
488 # to False. But those should just be the pre-selected states of a
487 # checkbox that the user could override if so desired. But I don't know
489 # checkbox that the user could override if so desired. But I don't know
488 # enough Qt to go implementing the checkbox now.
490 # enough Qt to go implementing the checkbox now.
489
491
490 if self.custom_restart:
492 if self.custom_restart:
491 self.custom_restart_requested.emit()
493 self.custom_restart_requested.emit()
492
494
493 elif self.kernel_manager.has_kernel:
495 elif self.kernel_manager.has_kernel:
494 # Pause the heart beat channel to prevent further warnings.
496 # Pause the heart beat channel to prevent further warnings.
495 self.kernel_manager.hb_channel.pause()
497 self.kernel_manager.hb_channel.pause()
496
498
497 # Prompt the user to restart the kernel. Un-pause the heartbeat if
499 # Prompt the user to restart the kernel. Un-pause the heartbeat if
498 # they decline. (If they accept, the heartbeat will be un-paused
500 # they decline. (If they accept, the heartbeat will be un-paused
499 # automatically when the kernel is restarted.)
501 # automatically when the kernel is restarted.)
500 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
502 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
501 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
503 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
502 message, buttons)
504 message, buttons)
503 if result == QtGui.QMessageBox.Yes:
505 if result == QtGui.QMessageBox.Yes:
504 try:
506 try:
505 self.kernel_manager.restart_kernel(now=now)
507 self.kernel_manager.restart_kernel(now=now)
506 except RuntimeError:
508 except RuntimeError:
507 self._append_plain_text('Kernel started externally. '
509 self._append_plain_text('Kernel started externally. '
508 'Cannot restart.\n')
510 'Cannot restart.\n')
509 else:
511 else:
510 self.reset()
512 self.reset()
511 else:
513 else:
512 self.kernel_manager.hb_channel.unpause()
514 self.kernel_manager.hb_channel.unpause()
513
515
514 else:
516 else:
515 self._append_plain_text('Kernel process is either remote or '
517 self._append_plain_text('Kernel process is either remote or '
516 'unspecified. Cannot restart.\n')
518 'unspecified. Cannot restart.\n')
517
519
518 #---------------------------------------------------------------------------
520 #---------------------------------------------------------------------------
519 # 'FrontendWidget' protected interface
521 # 'FrontendWidget' protected interface
520 #---------------------------------------------------------------------------
522 #---------------------------------------------------------------------------
521
523
522 def _call_tip(self):
524 def _call_tip(self):
523 """ Shows a call tip, if appropriate, at the current cursor location.
525 """ Shows a call tip, if appropriate, at the current cursor location.
524 """
526 """
525 # Decide if it makes sense to show a call tip
527 # Decide if it makes sense to show a call tip
526 if not self.enable_calltips:
528 if not self.enable_calltips:
527 return False
529 return False
528 cursor = self._get_cursor()
530 cursor = self._get_cursor()
529 cursor.movePosition(QtGui.QTextCursor.Left)
531 cursor.movePosition(QtGui.QTextCursor.Left)
530 if cursor.document().characterAt(cursor.position()) != '(':
532 if cursor.document().characterAt(cursor.position()) != '(':
531 return False
533 return False
532 context = self._get_context(cursor)
534 context = self._get_context(cursor)
533 if not context:
535 if not context:
534 return False
536 return False
535
537
536 # Send the metadata request to the kernel
538 # Send the metadata request to the kernel
537 name = '.'.join(context)
539 name = '.'.join(context)
538 msg_id = self.kernel_manager.shell_channel.object_info(name)
540 msg_id = self.kernel_manager.shell_channel.object_info(name)
539 pos = self._get_cursor().position()
541 pos = self._get_cursor().position()
540 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
542 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
541 return True
543 return True
542
544
543 def _complete(self):
545 def _complete(self):
544 """ Performs completion at the current cursor location.
546 """ Performs completion at the current cursor location.
545 """
547 """
546 context = self._get_context()
548 context = self._get_context()
547 if context:
549 if context:
548 # Send the completion request to the kernel
550 # Send the completion request to the kernel
549 msg_id = self.kernel_manager.shell_channel.complete(
551 msg_id = self.kernel_manager.shell_channel.complete(
550 '.'.join(context), # text
552 '.'.join(context), # text
551 self._get_input_buffer_cursor_line(), # line
553 self._get_input_buffer_cursor_line(), # line
552 self._get_input_buffer_cursor_column(), # cursor_pos
554 self._get_input_buffer_cursor_column(), # cursor_pos
553 self.input_buffer) # block
555 self.input_buffer) # block
554 pos = self._get_cursor().position()
556 pos = self._get_cursor().position()
555 info = self._CompletionRequest(msg_id, pos)
557 info = self._CompletionRequest(msg_id, pos)
556 self._request_info['complete'] = info
558 self._request_info['complete'] = info
557
559
558 def _get_context(self, cursor=None):
560 def _get_context(self, cursor=None):
559 """ Gets the context for the specified cursor (or the current cursor
561 """ Gets the context for the specified cursor (or the current cursor
560 if none is specified).
562 if none is specified).
561 """
563 """
562 if cursor is None:
564 if cursor is None:
563 cursor = self._get_cursor()
565 cursor = self._get_cursor()
564 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
566 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
565 QtGui.QTextCursor.KeepAnchor)
567 QtGui.QTextCursor.KeepAnchor)
566 text = cursor.selection().toPlainText()
568 text = cursor.selection().toPlainText()
567 return self._completion_lexer.get_context(text)
569 return self._completion_lexer.get_context(text)
568
570
569 def _process_execute_abort(self, msg):
571 def _process_execute_abort(self, msg):
570 """ Process a reply for an aborted execution request.
572 """ Process a reply for an aborted execution request.
571 """
573 """
572 self._append_plain_text("ERROR: execution aborted\n")
574 self._append_plain_text("ERROR: execution aborted\n")
573
575
574 def _process_execute_error(self, msg):
576 def _process_execute_error(self, msg):
575 """ Process a reply for an execution request that resulted in an error.
577 """ Process a reply for an execution request that resulted in an error.
576 """
578 """
577 content = msg['content']
579 content = msg['content']
578 # If a SystemExit is passed along, this means exit() was called - also
580 # If a SystemExit is passed along, this means exit() was called - also
579 # all the ipython %exit magic syntax of '-k' to be used to keep
581 # all the ipython %exit magic syntax of '-k' to be used to keep
580 # the kernel running
582 # the kernel running
581 if content['ename']=='SystemExit':
583 if content['ename']=='SystemExit':
582 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
584 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
583 self._keep_kernel_on_exit = keepkernel
585 self._keep_kernel_on_exit = keepkernel
584 self.exit_requested.emit()
586 self.exit_requested.emit()
585 else:
587 else:
586 traceback = ''.join(content['traceback'])
588 traceback = ''.join(content['traceback'])
587 self._append_plain_text(traceback)
589 self._append_plain_text(traceback)
588
590
589 def _process_execute_ok(self, msg):
591 def _process_execute_ok(self, msg):
590 """ Process a reply for a successful execution equest.
592 """ Process a reply for a successful execution equest.
591 """
593 """
592 payload = msg['content']['payload']
594 payload = msg['content']['payload']
593 for item in payload:
595 for item in payload:
594 if not self._process_execute_payload(item):
596 if not self._process_execute_payload(item):
595 warning = 'Warning: received unknown payload of type %s'
597 warning = 'Warning: received unknown payload of type %s'
596 print(warning % repr(item['source']))
598 print(warning % repr(item['source']))
597
599
598 def _process_execute_payload(self, item):
600 def _process_execute_payload(self, item):
599 """ Process a single payload item from the list of payload items in an
601 """ Process a single payload item from the list of payload items in an
600 execution reply. Returns whether the payload was handled.
602 execution reply. Returns whether the payload was handled.
601 """
603 """
602 # The basic FrontendWidget doesn't handle payloads, as they are a
604 # The basic FrontendWidget doesn't handle payloads, as they are a
603 # mechanism for going beyond the standard Python interpreter model.
605 # mechanism for going beyond the standard Python interpreter model.
604 return False
606 return False
605
607
606 def _show_interpreter_prompt(self):
608 def _show_interpreter_prompt(self):
607 """ Shows a prompt for the interpreter.
609 """ Shows a prompt for the interpreter.
608 """
610 """
609 self._show_prompt('>>> ')
611 self._show_prompt('>>> ')
610
612
611 def _show_interpreter_prompt_for_reply(self, msg):
613 def _show_interpreter_prompt_for_reply(self, msg):
612 """ Shows a prompt for the interpreter given an 'execute_reply' message.
614 """ Shows a prompt for the interpreter given an 'execute_reply' message.
613 """
615 """
614 self._show_interpreter_prompt()
616 self._show_interpreter_prompt()
615
617
616 #------ Signal handlers ----------------------------------------------------
618 #------ Signal handlers ----------------------------------------------------
617
619
618 def _document_contents_change(self, position, removed, added):
620 def _document_contents_change(self, position, removed, added):
619 """ Called whenever the document's content changes. Display a call tip
621 """ Called whenever the document's content changes. Display a call tip
620 if appropriate.
622 if appropriate.
621 """
623 """
622 # Calculate where the cursor should be *after* the change:
624 # Calculate where the cursor should be *after* the change:
623 position += added
625 position += added
624
626
625 document = self._control.document()
627 document = self._control.document()
626 if position == self._get_cursor().position():
628 if position == self._get_cursor().position():
627 self._call_tip()
629 self._call_tip()
628
630
629 #------ Trait default initializers -----------------------------------------
631 #------ Trait default initializers -----------------------------------------
630
632
631 def _banner_default(self):
633 def _banner_default(self):
632 """ Returns the standard Python banner.
634 """ Returns the standard Python banner.
633 """
635 """
634 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
636 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
635 '"license" for more information.'
637 '"license" for more information.'
636 return banner % (sys.version, sys.platform)
638 return banner % (sys.version, sys.platform)
@@ -1,270 +1,298 b''
1 # System library imports
1 # System library imports
2 from IPython.external.qt import QtGui
2 from IPython.external.qt import QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.utils.traitlets import Bool
5 from IPython.utils.traitlets import Bool
6 from console_widget import ConsoleWidget
6 from console_widget import ConsoleWidget
7
7
8
8
9 class HistoryConsoleWidget(ConsoleWidget):
9 class HistoryConsoleWidget(ConsoleWidget):
10 """ A ConsoleWidget that keeps a history of the commands that have been
10 """ A ConsoleWidget that keeps a history of the commands that have been
11 executed and provides a readline-esque interface to this history.
11 executed and provides a readline-esque interface to this history.
12 """
12 """
13
13
14 #------ Configuration ------------------------------------------------------
14 #------ Configuration ------------------------------------------------------
15
15
16 # If enabled, the input buffer will become "locked" to history movement when
16 # If enabled, the input buffer will become "locked" to history movement when
17 # an edit is made to a multi-line input buffer. To override the lock, use
17 # an edit is made to a multi-line input buffer. To override the lock, use
18 # Shift in conjunction with the standard history cycling keys.
18 # Shift in conjunction with the standard history cycling keys.
19 history_lock = Bool(False, config=True)
19 history_lock = Bool(False, config=True)
20
20
21 #---------------------------------------------------------------------------
21 #---------------------------------------------------------------------------
22 # 'object' interface
22 # 'object' interface
23 #---------------------------------------------------------------------------
23 #---------------------------------------------------------------------------
24
24
25 def __init__(self, *args, **kw):
25 def __init__(self, *args, **kw):
26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
27
27
28 # HistoryConsoleWidget protected variables.
28 # HistoryConsoleWidget protected variables.
29 self._history = []
29 self._history = []
30 self._history_edits = {}
30 self._history_edits = {}
31 self._history_index = 0
31 self._history_index = 0
32 self._history_prefix = ''
32 self._history_prefix = ''
33
33
34 self.history_action = QtGui.QAction("History",
34 self.history_action = QtGui.QAction("History",
35 self,
35 self,
36 statusTip="show command history",
36 statusTip="show command history",
37 triggered=self.history_magic)
37 triggered=self.history_magic)
38 self.addAction(self.history_action)
38 self.addAction(self.history_action)
39
39
40 self.save_action = QtGui.QAction("Export History ",
40 self.save_action = QtGui.QAction("Export History ",
41 self,
41 self,
42 statusTip="Export History as Python File",
42 statusTip="Export History as Python File",
43 triggered=self.save_magic)
43 triggered=self.save_magic)
44 self.addAction(self.save_action)
44 self.addAction(self.save_action)
45
45
46 #---------------------------------------------------------------------------
46 #---------------------------------------------------------------------------
47 # 'ConsoleWidget' public interface
47 # 'ConsoleWidget' public interface
48 #---------------------------------------------------------------------------
48 #---------------------------------------------------------------------------
49
49
50 def execute(self, source=None, hidden=False, interactive=False):
50 def execute(self, source=None, hidden=False, interactive=False):
51 """ Reimplemented to the store history.
51 """ Reimplemented to the store history.
52 """
52 """
53 if not hidden:
53 if not hidden:
54 history = self.input_buffer if source is None else source
54 history = self.input_buffer if source is None else source
55
55
56 executed = super(HistoryConsoleWidget, self).execute(
56 executed = super(HistoryConsoleWidget, self).execute(
57 source, hidden, interactive)
57 source, hidden, interactive)
58
58
59 if executed and not hidden:
59 if executed and not hidden:
60 # Save the command unless it was an empty string or was identical
60 # Save the command unless it was an empty string or was identical
61 # to the previous command.
61 # to the previous command.
62 history = history.rstrip()
62 history = history.rstrip()
63 if history and (not self._history or self._history[-1] != history):
63 if history and (not self._history or self._history[-1] != history):
64 self._history.append(history)
64 self._history.append(history)
65
65
66 # Emulate readline: reset all history edits.
66 # Emulate readline: reset all history edits.
67 self._history_edits = {}
67 self._history_edits = {}
68
68
69 # Move the history index to the most recent item.
69 # Move the history index to the most recent item.
70 self._history_index = len(self._history)
70 self._history_index = len(self._history)
71
71
72 return executed
72 return executed
73
73
74 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
75 # 'ConsoleWidget' abstract interface
75 # 'ConsoleWidget' abstract interface
76 #---------------------------------------------------------------------------
76 #---------------------------------------------------------------------------
77
77
78 def _up_pressed(self, shift_modifier):
78 def _up_pressed(self, shift_modifier):
79 """ Called when the up key is pressed. Returns whether to continue
79 """ Called when the up key is pressed. Returns whether to continue
80 processing the event.
80 processing the event.
81 """
81 """
82 prompt_cursor = self._get_prompt_cursor()
82 prompt_cursor = self._get_prompt_cursor()
83 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
83 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
84 # Bail out if we're locked.
84 # Bail out if we're locked.
85 if self._history_locked() and not shift_modifier:
85 if self._history_locked() and not shift_modifier:
86 return False
86 return False
87
87
88 # Set a search prefix based on the cursor position.
88 # Set a search prefix based on the cursor position.
89 col = self._get_input_buffer_cursor_column()
89 col = self._get_input_buffer_cursor_column()
90 input_buffer = self.input_buffer
90 input_buffer = self.input_buffer
91 if self._history_index == len(self._history) or \
91 if self._history_index == len(self._history) or \
92 (self._history_prefix and col != len(self._history_prefix)):
92 (self._history_prefix and col != len(self._history_prefix)):
93 self._history_index = len(self._history)
93 self._history_index = len(self._history)
94 self._history_prefix = input_buffer[:col]
94 self._history_prefix = input_buffer[:col]
95
95
96 # Perform the search.
96 # Perform the search.
97 self.history_previous(self._history_prefix,
97 self.history_previous(self._history_prefix,
98 as_prefix=not shift_modifier)
98 as_prefix=not shift_modifier)
99
99
100 # Go to the first line of the prompt for seemless history scrolling.
100 # Go to the first line of the prompt for seemless history scrolling.
101 # Emulate readline: keep the cursor position fixed for a prefix
101 # Emulate readline: keep the cursor position fixed for a prefix
102 # search.
102 # search.
103 cursor = self._get_prompt_cursor()
103 cursor = self._get_prompt_cursor()
104 if self._history_prefix:
104 if self._history_prefix:
105 cursor.movePosition(QtGui.QTextCursor.Right,
105 cursor.movePosition(QtGui.QTextCursor.Right,
106 n=len(self._history_prefix))
106 n=len(self._history_prefix))
107 else:
107 else:
108 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
108 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
109 self._set_cursor(cursor)
109 self._set_cursor(cursor)
110
110
111 return False
111 return False
112
112
113 return True
113 return True
114
114
115 def _down_pressed(self, shift_modifier):
115 def _down_pressed(self, shift_modifier):
116 """ Called when the down key is pressed. Returns whether to continue
116 """ Called when the down key is pressed. Returns whether to continue
117 processing the event.
117 processing the event.
118 """
118 """
119 end_cursor = self._get_end_cursor()
119 end_cursor = self._get_end_cursor()
120 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
120 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
121 # Bail out if we're locked.
121 # Bail out if we're locked.
122 if self._history_locked() and not shift_modifier:
122 if self._history_locked() and not shift_modifier:
123 return False
123 return False
124
124
125 # Perform the search.
125 # Perform the search.
126 replaced = self.history_next(self._history_prefix,
126 replaced = self.history_next(self._history_prefix,
127 as_prefix=not shift_modifier)
127 as_prefix=not shift_modifier)
128
128
129 # Emulate readline: keep the cursor position fixed for a prefix
129 # Emulate readline: keep the cursor position fixed for a prefix
130 # search. (We don't need to move the cursor to the end of the buffer
130 # search. (We don't need to move the cursor to the end of the buffer
131 # in the other case because this happens automatically when the
131 # in the other case because this happens automatically when the
132 # input buffer is set.)
132 # input buffer is set.)
133 if self._history_prefix and replaced:
133 if self._history_prefix and replaced:
134 cursor = self._get_prompt_cursor()
134 cursor = self._get_prompt_cursor()
135 cursor.movePosition(QtGui.QTextCursor.Right,
135 cursor.movePosition(QtGui.QTextCursor.Right,
136 n=len(self._history_prefix))
136 n=len(self._history_prefix))
137 self._set_cursor(cursor)
137 self._set_cursor(cursor)
138
138
139 return False
139 return False
140
140
141 return True
141 return True
142
142
143 #---------------------------------------------------------------------------
143 #---------------------------------------------------------------------------
144 # 'HistoryConsoleWidget' public interface
144 # 'HistoryConsoleWidget' public interface
145 #---------------------------------------------------------------------------
145 #---------------------------------------------------------------------------
146
146
147 def history_previous(self, substring='', as_prefix=True):
147 def history_previous(self, substring='', as_prefix=True):
148 """ If possible, set the input buffer to a previous history item.
148 """ If possible, set the input buffer to a previous history item.
149
149
150 Parameters:
150 Parameters:
151 -----------
151 -----------
152 substring : str, optional
152 substring : str, optional
153 If specified, search for an item with this substring.
153 If specified, search for an item with this substring.
154 as_prefix : bool, optional
154 as_prefix : bool, optional
155 If True, the substring must match at the beginning (default).
155 If True, the substring must match at the beginning (default).
156
156
157 Returns:
157 Returns:
158 --------
158 --------
159 Whether the input buffer was changed.
159 Whether the input buffer was changed.
160 """
160 """
161 index = self._history_index
161 index = self._history_index
162 replace = False
162 replace = False
163 while index > 0:
163 while index > 0:
164 index -= 1
164 index -= 1
165 history = self._get_edited_history(index)
165 history = self._get_edited_history(index)
166 if (as_prefix and history.startswith(substring)) \
166 if (as_prefix and history.startswith(substring)) \
167 or (not as_prefix and substring in history):
167 or (not as_prefix and substring in history):
168 replace = True
168 replace = True
169 break
169 break
170
170
171 if replace:
171 if replace:
172 self._store_edits()
172 self._store_edits()
173 self._history_index = index
173 self._history_index = index
174 self.input_buffer = history
174 self.input_buffer = history
175
175
176 return replace
176 return replace
177
177
178 def history_next(self, substring='', as_prefix=True):
178 def history_next(self, substring='', as_prefix=True):
179 """ If possible, set the input buffer to a subsequent history item.
179 """ If possible, set the input buffer to a subsequent history item.
180
180
181 Parameters:
181 Parameters:
182 -----------
182 -----------
183 substring : str, optional
183 substring : str, optional
184 If specified, search for an item with this substring.
184 If specified, search for an item with this substring.
185 as_prefix : bool, optional
185 as_prefix : bool, optional
186 If True, the substring must match at the beginning (default).
186 If True, the substring must match at the beginning (default).
187
187
188 Returns:
188 Returns:
189 --------
189 --------
190 Whether the input buffer was changed.
190 Whether the input buffer was changed.
191 """
191 """
192 index = self._history_index
192 index = self._history_index
193 replace = False
193 replace = False
194 while self._history_index < len(self._history):
194 while self._history_index < len(self._history):
195 index += 1
195 index += 1
196 history = self._get_edited_history(index)
196 history = self._get_edited_history(index)
197 if (as_prefix and history.startswith(substring)) \
197 if (as_prefix and history.startswith(substring)) \
198 or (not as_prefix and substring in history):
198 or (not as_prefix and substring in history):
199 replace = True
199 replace = True
200 break
200 break
201
201
202 if replace:
202 if replace:
203 self._store_edits()
203 self._store_edits()
204 self._history_index = index
204 self._history_index = index
205 self.input_buffer = history
205 self.input_buffer = history
206
206
207 return replace
207 return replace
208
208
209 def history_tail(self, n=10):
209 def history_tail(self, n=10):
210 """ Get the local history list.
210 """ Get the local history list.
211
211
212 Parameters:
212 Parameters:
213 -----------
213 -----------
214 n : int
214 n : int
215 The (maximum) number of history items to get.
215 The (maximum) number of history items to get.
216 """
216 """
217 return self._history[-n:]
217 return self._history[-n:]
218
218
219 def history_magic(self):
219 def history_magic(self):
220 self.pasteMagic("%history")
220 self.pasteMagic("%history")
221
221
222 def _request_update_session_history_length(self):
223 msg_id = self.kernel_manager.shell_channel.execute('',
224 silent=True,
225 user_expressions={
226 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
227 }
228 )
229 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'save_magic')
230
231 def _handle_execute_reply(self, msg):
232 """ Handles replies for code execution, here only session history length
233 """
234 info = self._request_info.get('execute')
235 if info and info.id == msg['parent_header']['msg_id'] and \
236 info.kind == 'save_magic' and not self._hidden:
237 content = msg['content']
238 status = content['status']
239 if status == 'ok':
240 self._max_session_history=(int(content['user_expressions']['hlen']))
241
222 def save_magic(self):
242 def save_magic(self):
223 file_name, ok = QtGui.QInputDialog.getText(self,
243 # update the session history length
224 'Enter A file Name',
244 self._request_update_session_history_length()
225 'Please enter a filename to wich export the history as python file:',
245
226 text='untilted.py')
246 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
227 if ok:
247 "Enter A filename",
248 filter='Python File (*.py);; All files (*.*)'
249 )
250
251 # let's the user search/type for a file name, while the history length
252 # is fetched
253
254 if file_name:
228 hist_range, ok = QtGui.QInputDialog.getText(self,
255 hist_range, ok = QtGui.QInputDialog.getText(self,
229 'Please enter an interval of command to save',
256 'Please enter an interval of command to save',
230 'Saving commands:',
257 'Saving commands:',
231 text='1-500')
258 text=str('1-'+str(self._max_session_history))
259 )
232 if ok:
260 if ok:
233 self.pasteMagic("%save"+" "+file_name+" "+str(hist_range))
261 self.pasteMagic("%save"+" "+file_name+" "+str(hist_range))
234
262
235 #---------------------------------------------------------------------------
263 #---------------------------------------------------------------------------
236 # 'HistoryConsoleWidget' protected interface
264 # 'HistoryConsoleWidget' protected interface
237 #---------------------------------------------------------------------------
265 #---------------------------------------------------------------------------
238
266
239 def _history_locked(self):
267 def _history_locked(self):
240 """ Returns whether history movement is locked.
268 """ Returns whether history movement is locked.
241 """
269 """
242 return (self.history_lock and
270 return (self.history_lock and
243 (self._get_edited_history(self._history_index) !=
271 (self._get_edited_history(self._history_index) !=
244 self.input_buffer) and
272 self.input_buffer) and
245 (self._get_prompt_cursor().blockNumber() !=
273 (self._get_prompt_cursor().blockNumber() !=
246 self._get_end_cursor().blockNumber()))
274 self._get_end_cursor().blockNumber()))
247
275
248 def _get_edited_history(self, index):
276 def _get_edited_history(self, index):
249 """ Retrieves a history item, possibly with temporary edits.
277 """ Retrieves a history item, possibly with temporary edits.
250 """
278 """
251 if index in self._history_edits:
279 if index in self._history_edits:
252 return self._history_edits[index]
280 return self._history_edits[index]
253 elif index == len(self._history):
281 elif index == len(self._history):
254 return unicode()
282 return unicode()
255 return self._history[index]
283 return self._history[index]
256
284
257 def _set_history(self, history):
285 def _set_history(self, history):
258 """ Replace the current history with a sequence of history items.
286 """ Replace the current history with a sequence of history items.
259 """
287 """
260 self._history = list(history)
288 self._history = list(history)
261 self._history_edits = {}
289 self._history_edits = {}
262 self._history_index = len(self._history)
290 self._history_index = len(self._history)
263
291
264 def _store_edits(self):
292 def _store_edits(self):
265 """ If there are edits to the current input buffer, store them.
293 """ If there are edits to the current input buffer, store them.
266 """
294 """
267 current = self.input_buffer
295 current = self.input_buffer
268 if self._history_index == len(self._history) or \
296 if self._history_index == len(self._history) or \
269 self._history[self._history_index] != current:
297 self._history[self._history_index] != current:
270 self._history_edits[self._history_index] = current
298 self._history_edits[self._history_index] = current
General Comments 0
You need to be logged in to leave comments. Login now