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