##// END OF EJS Templates
Fixed ANSI compliance issue in AnsiCodeProcessor....
epatters -
Show More
@@ -1,230 +1,233 b''
1 1 """ Utilities for processing ANSI escape codes and special ASCII characters.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 from collections import namedtuple
9 9 import re
10 10
11 11 # System library imports
12 12 from PyQt4 import QtCore, QtGui
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Constants and datatypes
16 16 #-----------------------------------------------------------------------------
17 17
18 18 # An action for erase requests (ED and EL commands).
19 19 EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
20 20
21 21 # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
22 22 # and HVP commands).
23 23 # FIXME: Not implemented in AnsiCodeProcessor.
24 24 MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
25 25
26 26 # An action for scroll requests (SU and ST) and form feeds.
27 27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Classes
31 31 #-----------------------------------------------------------------------------
32 32
33 33 class AnsiCodeProcessor(object):
34 34 """ Translates special ASCII characters and ANSI escape codes into readable
35 35 attributes.
36 36 """
37 37
38 38 # Whether to increase intensity or set boldness for SGR code 1.
39 39 # (Different terminals handle this in different ways.)
40 40 bold_text_enabled = False
41 41
42 42 # Protected class variables.
43 43 _ansi_commands = 'ABCDEFGHJKSTfmnsu'
44 44 _ansi_pattern = re.compile('\x01?\x1b\[(.*?)([%s])\x02?' % _ansi_commands)
45 45 _special_pattern = re.compile('([\f])')
46 46
47 47 #---------------------------------------------------------------------------
48 48 # AnsiCodeProcessor interface
49 49 #---------------------------------------------------------------------------
50 50
51 51 def __init__(self):
52 52 self.actions = []
53 53 self.reset_sgr()
54 54
55 55 def reset_sgr(self):
56 56 """ Reset graphics attributs to their default values.
57 57 """
58 58 self.intensity = 0
59 59 self.italic = False
60 60 self.bold = False
61 61 self.underline = False
62 62 self.foreground_color = None
63 63 self.background_color = None
64 64
65 65 def split_string(self, string):
66 66 """ Yields substrings for which the same escape code applies.
67 67 """
68 68 self.actions = []
69 69 start = 0
70 70
71 71 for match in self._ansi_pattern.finditer(string):
72 72 raw = string[start:match.start()]
73 73 substring = self._special_pattern.sub(self._replace_special, raw)
74 74 if substring or self.actions:
75 75 yield substring
76 76 start = match.end()
77 77
78 78 self.actions = []
79 79 try:
80 80 params = []
81 81 for param in match.group(1).split(';'):
82 82 if param:
83 83 params.append(int(param))
84 84 except ValueError:
85 85 # Silently discard badly formed escape codes.
86 86 pass
87 87 else:
88 88 self.set_csi_code(match.group(2), params)
89 89
90 90 raw = string[start:]
91 91 substring = self._special_pattern.sub(self._replace_special, raw)
92 92 if substring or self.actions:
93 93 yield substring
94 94
95 95 def set_csi_code(self, command, params=[]):
96 96 """ Set attributes based on CSI (Control Sequence Introducer) code.
97 97
98 98 Parameters
99 99 ----------
100 100 command : str
101 101 The code identifier, i.e. the final character in the sequence.
102 102
103 103 params : sequence of integers, optional
104 104 The parameter codes for the command.
105 105 """
106 106 if command == 'm': # SGR - Select Graphic Rendition
107 for code in params:
108 self.set_sgr_code(code)
107 if params:
108 for code in params:
109 self.set_sgr_code(code)
110 else:
111 self.set_sgr_code(0)
109 112
110 113 elif (command == 'J' or # ED - Erase Data
111 114 command == 'K'): # EL - Erase in Line
112 115 code = params[0] if params else 0
113 116 if 0 <= code <= 2:
114 117 area = 'screen' if command == 'J' else 'line'
115 118 if code == 0:
116 119 erase_to = 'end'
117 120 elif code == 1:
118 121 erase_to = 'start'
119 122 elif code == 2:
120 123 erase_to = 'all'
121 124 self.actions.append(EraseAction('erase', area, erase_to))
122 125
123 126 elif (command == 'S' or # SU - Scroll Up
124 127 command == 'T'): # SD - Scroll Down
125 128 dir = 'up' if command == 'S' else 'down'
126 129 count = params[0] if params else 1
127 130 self.actions.append(ScrollAction('scroll', dir, 'line', count))
128 131
129 132 def set_sgr_code(self, code):
130 133 """ Set attributes based on SGR (Select Graphic Rendition) code.
131 134 """
132 135 if code == 0:
133 136 self.reset_sgr()
134 137 elif code == 1:
135 138 if self.bold_text_enabled:
136 139 self.bold = True
137 140 else:
138 141 self.intensity = 1
139 142 elif code == 2:
140 143 self.intensity = 0
141 144 elif code == 3:
142 145 self.italic = True
143 146 elif code == 4:
144 147 self.underline = True
145 148 elif code == 22:
146 149 self.intensity = 0
147 150 self.bold = False
148 151 elif code == 23:
149 152 self.italic = False
150 153 elif code == 24:
151 154 self.underline = False
152 155 elif code >= 30 and code <= 37:
153 156 self.foreground_color = code - 30
154 157 elif code == 39:
155 158 self.foreground_color = None
156 159 elif code >= 40 and code <= 47:
157 160 self.background_color = code - 40
158 161 elif code == 49:
159 162 self.background_color = None
160 163
161 164 #---------------------------------------------------------------------------
162 165 # Protected interface
163 166 #---------------------------------------------------------------------------
164 167
165 168 def _replace_special(self, match):
166 169 special = match.group(1)
167 170 if special == '\f':
168 171 self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
169 172 return ''
170 173
171 174
172 175 class QtAnsiCodeProcessor(AnsiCodeProcessor):
173 176 """ Translates ANSI escape codes into QTextCharFormats.
174 177 """
175 178
176 179 # A map from color codes to RGB colors.
177 180 default_map = (# Normal, Bright/Light ANSI color code
178 181 ('black', 'grey'), # 0: black
179 182 ('darkred', 'red'), # 1: red
180 183 ('darkgreen', 'lime'), # 2: green
181 184 ('brown', 'yellow'), # 3: yellow
182 185 ('darkblue', 'deepskyblue'), # 4: blue
183 186 ('darkviolet', 'magenta'), # 5: magenta
184 187 ('steelblue', 'cyan'), # 6: cyan
185 188 ('grey', 'white')) # 7: white
186 189
187 190 def __init__(self):
188 191 super(QtAnsiCodeProcessor, self).__init__()
189 192 self.color_map = self.default_map
190 193
191 194 def get_format(self):
192 195 """ Returns a QTextCharFormat that encodes the current style attributes.
193 196 """
194 197 format = QtGui.QTextCharFormat()
195 198
196 199 # Set foreground color
197 200 if self.foreground_color is not None:
198 201 color = self.color_map[self.foreground_color][self.intensity]
199 202 format.setForeground(QtGui.QColor(color))
200 203
201 204 # Set background color
202 205 if self.background_color is not None:
203 206 color = self.color_map[self.background_color][self.intensity]
204 207 format.setBackground(QtGui.QColor(color))
205 208
206 209 # Set font weight/style options
207 210 if self.bold:
208 211 format.setFontWeight(QtGui.QFont.Bold)
209 212 else:
210 213 format.setFontWeight(QtGui.QFont.Normal)
211 214 format.setFontItalic(self.italic)
212 215 format.setFontUnderline(self.underline)
213 216
214 217 return format
215 218
216 219 def set_background_color(self, color):
217 220 """ Given a background color (a QColor), attempt to set a color map
218 221 that will be aesthetically pleasing.
219 222 """
220 223 if color.value() < 127:
221 224 # Colors appropriate for a terminal with a dark background.
222 225 self.color_map = self.default_map
223 226
224 227 else:
225 228 # Colors appropriate for a terminal with a light background. For
226 229 # now, only use non-bright colors...
227 230 self.color_map = [ (pair[0], pair[0]) for pair in self.default_map ]
228 231
229 232 # ...and replace white with black.
230 233 self.color_map[7] = ('black', 'black')
@@ -1,524 +1,530 b''
1 1 # Standard library imports
2 2 from collections import namedtuple
3 3 import signal
4 4 import sys
5 5
6 6 # System library imports
7 7 from pygments.lexers import PythonLexer
8 8 from PyQt4 import QtCore, QtGui
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
12 12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 13 from IPython.utils.io import raw_print
14 14 from IPython.utils.traitlets import Bool
15 15 from bracket_matcher import BracketMatcher
16 16 from call_tip_widget import CallTipWidget
17 17 from completion_lexer import CompletionLexer
18 18 from history_console_widget import HistoryConsoleWidget
19 19 from pygments_highlighter import PygmentsHighlighter
20 20
21 21
22 22 class FrontendHighlighter(PygmentsHighlighter):
23 23 """ A PygmentsHighlighter that can be turned on and off and that ignores
24 24 prompts.
25 25 """
26 26
27 27 def __init__(self, frontend):
28 28 super(FrontendHighlighter, self).__init__(frontend._control.document())
29 29 self._current_offset = 0
30 30 self._frontend = frontend
31 31 self.highlighting_on = False
32 32
33 33 def highlightBlock(self, qstring):
34 34 """ Highlight a block of text. Reimplemented to highlight selectively.
35 35 """
36 36 if not self.highlighting_on:
37 37 return
38 38
39 39 # The input to this function is unicode string that may contain
40 40 # paragraph break characters, non-breaking spaces, etc. Here we acquire
41 41 # the string as plain text so we can compare it.
42 42 current_block = self.currentBlock()
43 43 string = self._frontend._get_block_plain_text(current_block)
44 44
45 45 # Decide whether to check for the regular or continuation prompt.
46 46 if current_block.contains(self._frontend._prompt_pos):
47 47 prompt = self._frontend._prompt
48 48 else:
49 49 prompt = self._frontend._continuation_prompt
50 50
51 51 # Don't highlight the part of the string that contains the prompt.
52 52 if string.startswith(prompt):
53 53 self._current_offset = len(prompt)
54 54 qstring.remove(0, len(prompt))
55 55 else:
56 56 self._current_offset = 0
57 57
58 58 PygmentsHighlighter.highlightBlock(self, qstring)
59 59
60 60 def rehighlightBlock(self, block):
61 61 """ Reimplemented to temporarily enable highlighting if disabled.
62 62 """
63 63 old = self.highlighting_on
64 64 self.highlighting_on = True
65 65 super(FrontendHighlighter, self).rehighlightBlock(block)
66 66 self.highlighting_on = old
67 67
68 68 def setFormat(self, start, count, format):
69 69 """ Reimplemented to highlight selectively.
70 70 """
71 71 start += self._current_offset
72 72 PygmentsHighlighter.setFormat(self, start, count, format)
73 73
74 74
75 75 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 76 """ A Qt frontend for a generic Python kernel.
77 77 """
78 78
79 79 # An option and corresponding signal for overriding the default kernel
80 80 # interrupt behavior.
81 81 custom_interrupt = Bool(False)
82 82 custom_interrupt_requested = QtCore.pyqtSignal()
83 83
84 84 # An option and corresponding signals for overriding the default kernel
85 85 # restart behavior.
86 86 custom_restart = Bool(False)
87 87 custom_restart_kernel_died = QtCore.pyqtSignal(float)
88 88 custom_restart_requested = QtCore.pyqtSignal()
89 89
90 90 # Emitted when an 'execute_reply' has been received from the kernel and
91 91 # processed by the FrontendWidget.
92 92 executed = QtCore.pyqtSignal(object)
93 93
94 94 # Emitted when an exit request has been received from the kernel.
95 95 exit_requested = QtCore.pyqtSignal()
96 96
97 97 # Protected class variables.
98 98 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
99 99 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
100 100 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
101 101 _input_splitter_class = InputSplitter
102 102
103 103 #---------------------------------------------------------------------------
104 104 # 'object' interface
105 105 #---------------------------------------------------------------------------
106 106
107 107 def __init__(self, *args, **kw):
108 108 super(FrontendWidget, self).__init__(*args, **kw)
109 109
110 110 # FrontendWidget protected variables.
111 111 self._bracket_matcher = BracketMatcher(self._control)
112 112 self._call_tip_widget = CallTipWidget(self._control)
113 113 self._completion_lexer = CompletionLexer(PythonLexer())
114 114 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
115 115 self._hidden = False
116 116 self._highlighter = FrontendHighlighter(self)
117 117 self._input_splitter = self._input_splitter_class(input_mode='block')
118 118 self._kernel_manager = None
119 119 self._possible_kernel_restart = False
120 120 self._request_info = {}
121 121
122 122 # Configure the ConsoleWidget.
123 123 self.tab_width = 4
124 124 self._set_continuation_prompt('... ')
125 125
126 126 # Configure actions.
127 127 action = self._copy_raw_action
128 128 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
129 129 action.setEnabled(False)
130 130 action.setShortcut(QtGui.QKeySequence(key))
131 131 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
132 132 action.triggered.connect(self.copy_raw)
133 133 self.copy_available.connect(action.setEnabled)
134 134 self.addAction(action)
135 135
136 136 # Connect signal handlers.
137 137 document = self._control.document()
138 138 document.contentsChange.connect(self._document_contents_change)
139 139
140 140 #---------------------------------------------------------------------------
141 141 # 'ConsoleWidget' public interface
142 142 #---------------------------------------------------------------------------
143 143
144 144 def copy(self):
145 145 """ Copy the currently selected text to the clipboard, removing prompts.
146 146 """
147 147 text = str(self._control.textCursor().selection().toPlainText())
148 148 if text:
149 149 # Remove prompts.
150 150 lines = map(transform_classic_prompt, text.splitlines())
151 151 text = '\n'.join(lines)
152 152 # Expand tabs so that we respect PEP-8.
153 153 QtGui.QApplication.clipboard().setText(text.expandtabs(4))
154 154
155 155 #---------------------------------------------------------------------------
156 156 # 'ConsoleWidget' abstract interface
157 157 #---------------------------------------------------------------------------
158 158
159 159 def _is_complete(self, source, interactive):
160 160 """ Returns whether 'source' can be completely processed and a new
161 161 prompt created. When triggered by an Enter/Return key press,
162 162 'interactive' is True; otherwise, it is False.
163 163 """
164 164 complete = self._input_splitter.push(source.expandtabs(4))
165 165 if interactive:
166 166 complete = not self._input_splitter.push_accepts_more()
167 167 return complete
168 168
169 169 def _execute(self, source, hidden):
170 170 """ Execute 'source'. If 'hidden', do not show any output.
171 171
172 172 See parent class :meth:`execute` docstring for full details.
173 173 """
174 174 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
175 175 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
176 176 self._hidden = hidden
177 177
178 178 def _prompt_started_hook(self):
179 179 """ Called immediately after a new prompt is displayed.
180 180 """
181 181 if not self._reading:
182 182 self._highlighter.highlighting_on = True
183 183
184 184 def _prompt_finished_hook(self):
185 185 """ Called immediately after a prompt is finished, i.e. when some input
186 186 will be processed and a new prompt displayed.
187 187 """
188 188 if not self._reading:
189 189 self._highlighter.highlighting_on = False
190 190
191 191 def _tab_pressed(self):
192 192 """ Called when the tab key is pressed. Returns whether to continue
193 193 processing the event.
194 194 """
195 195 # Perform tab completion if:
196 196 # 1) The cursor is in the input buffer.
197 197 # 2) There is a non-whitespace character before the cursor.
198 198 text = self._get_input_buffer_cursor_line()
199 199 if text is None:
200 200 return False
201 201 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
202 202 if complete:
203 203 self._complete()
204 204 return not complete
205 205
206 206 #---------------------------------------------------------------------------
207 207 # 'ConsoleWidget' protected interface
208 208 #---------------------------------------------------------------------------
209 209
210 210 def _context_menu_make(self, pos):
211 211 """ Reimplemented to add an action for raw copy.
212 212 """
213 213 menu = super(FrontendWidget, self)._context_menu_make(pos)
214 214 for before_action in menu.actions():
215 215 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
216 216 QtGui.QKeySequence.ExactMatch:
217 217 menu.insertAction(before_action, self._copy_raw_action)
218 218 break
219 219 return menu
220 220
221 221 def _event_filter_console_keypress(self, event):
222 222 """ Reimplemented to allow execution interruption.
223 223 """
224 224 key = event.key()
225 225 if self._control_key_down(event.modifiers(), include_command=False):
226 226 if key == QtCore.Qt.Key_C and self._executing:
227 227 self.interrupt_kernel()
228 228 return True
229 229 elif key == QtCore.Qt.Key_Period:
230 230 message = 'Are you sure you want to restart the kernel?'
231 231 self.restart_kernel(message, instant_death=False)
232 232 return True
233 233 return super(FrontendWidget, self)._event_filter_console_keypress(event)
234 234
235 235 def _insert_continuation_prompt(self, cursor):
236 236 """ Reimplemented for auto-indentation.
237 237 """
238 238 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
239 239 spaces = self._input_splitter.indent_spaces
240 240 cursor.insertText('\t' * (spaces / self.tab_width))
241 241 cursor.insertText(' ' * (spaces % self.tab_width))
242 242
243 243 #---------------------------------------------------------------------------
244 244 # 'BaseFrontendMixin' abstract interface
245 245 #---------------------------------------------------------------------------
246 246
247 247 def _handle_complete_reply(self, rep):
248 248 """ Handle replies for tab completion.
249 249 """
250 250 cursor = self._get_cursor()
251 251 info = self._request_info.get('complete')
252 252 if info and info.id == rep['parent_header']['msg_id'] and \
253 253 info.pos == cursor.position():
254 254 text = '.'.join(self._get_context())
255 255 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
256 256 self._complete_with_items(cursor, rep['content']['matches'])
257 257
258 258 def _handle_execute_reply(self, msg):
259 259 """ Handles replies for code execution.
260 260 """
261 261 info = self._request_info.get('execute')
262 262 if info and info.id == msg['parent_header']['msg_id'] and \
263 263 info.kind == 'user' and not self._hidden:
264 264 # Make sure that all output from the SUB channel has been processed
265 265 # before writing a new prompt.
266 266 self.kernel_manager.sub_channel.flush()
267 267
268 # Reset the ANSI style information to prevent bad text in stdout
269 # from messing up our colors. We're not a true terminal so we're
270 # allowed to do this.
271 if self.ansi_codes:
272 self._ansi_processor.reset_sgr()
273
268 274 content = msg['content']
269 275 status = content['status']
270 276 if status == 'ok':
271 277 self._process_execute_ok(msg)
272 278 elif status == 'error':
273 279 self._process_execute_error(msg)
274 280 elif status == 'abort':
275 281 self._process_execute_abort(msg)
276 282
277 283 self._show_interpreter_prompt_for_reply(msg)
278 284 self.executed.emit(msg)
279 285
280 286 def _handle_input_request(self, msg):
281 287 """ Handle requests for raw_input.
282 288 """
283 289 if self._hidden:
284 290 raise RuntimeError('Request for raw input during hidden execution.')
285 291
286 292 # Make sure that all output from the SUB channel has been processed
287 293 # before entering readline mode.
288 294 self.kernel_manager.sub_channel.flush()
289 295
290 296 def callback(line):
291 297 self.kernel_manager.rep_channel.input(line)
292 298 self._readline(msg['content']['prompt'], callback=callback)
293 299
294 300 def _handle_kernel_died(self, since_last_heartbeat):
295 301 """ Handle the kernel's death by asking if the user wants to restart.
296 302 """
297 303 message = 'The kernel heartbeat has been inactive for %.2f ' \
298 304 'seconds. Do you want to restart the kernel? You may ' \
299 305 'first want to check the network connection.' % \
300 306 since_last_heartbeat
301 307 if self.custom_restart:
302 308 self.custom_restart_kernel_died.emit(since_last_heartbeat)
303 309 else:
304 310 self.restart_kernel(message, instant_death=True)
305 311
306 312 def _handle_object_info_reply(self, rep):
307 313 """ Handle replies for call tips.
308 314 """
309 315 cursor = self._get_cursor()
310 316 info = self._request_info.get('call_tip')
311 317 if info and info.id == rep['parent_header']['msg_id'] and \
312 318 info.pos == cursor.position():
313 319 doc = rep['content']['docstring']
314 320 if doc:
315 321 self._call_tip_widget.show_docstring(doc)
316 322
317 323 def _handle_pyout(self, msg):
318 324 """ Handle display hook output.
319 325 """
320 326 if not self._hidden and self._is_from_this_session(msg):
321 327 self._append_plain_text(msg['content']['data'] + '\n')
322 328
323 329 def _handle_stream(self, msg):
324 330 """ Handle stdout, stderr, and stdin.
325 331 """
326 332 if not self._hidden and self._is_from_this_session(msg):
327 333 # Most consoles treat tabs as being 8 space characters. Convert tabs
328 334 # to spaces so that output looks as expected regardless of this
329 335 # widget's tab width.
330 336 text = msg['content']['data'].expandtabs(8)
331 337
332 338 self._append_plain_text(text)
333 339 self._control.moveCursor(QtGui.QTextCursor.End)
334 340
335 341 def _started_channels(self):
336 342 """ Called when the KernelManager channels have started listening or
337 343 when the frontend is assigned an already listening KernelManager.
338 344 """
339 345 self._control.clear()
340 346 self._append_plain_text(self._get_banner())
341 347 self._show_interpreter_prompt()
342 348
343 349 def _stopped_channels(self):
344 350 """ Called when the KernelManager channels have stopped listening or
345 351 when a listening KernelManager is removed from the frontend.
346 352 """
347 353 self._executing = self._reading = False
348 354 self._highlighter.highlighting_on = False
349 355
350 356 #---------------------------------------------------------------------------
351 357 # 'FrontendWidget' public interface
352 358 #---------------------------------------------------------------------------
353 359
354 360 def copy_raw(self):
355 361 """ Copy the currently selected text to the clipboard without attempting
356 362 to remove prompts or otherwise alter the text.
357 363 """
358 364 self._control.copy()
359 365
360 366 def execute_file(self, path, hidden=False):
361 367 """ Attempts to execute file with 'path'. If 'hidden', no output is
362 368 shown.
363 369 """
364 370 self.execute('execfile("%s")' % path, hidden=hidden)
365 371
366 372 def interrupt_kernel(self):
367 373 """ Attempts to interrupt the running kernel.
368 374 """
369 375 if self.custom_interrupt:
370 376 self.custom_interrupt_requested.emit()
371 377 elif self.kernel_manager.has_kernel:
372 378 self.kernel_manager.signal_kernel(signal.SIGINT)
373 379 else:
374 380 self._append_plain_text('Kernel process is either remote or '
375 381 'unspecified. Cannot interrupt.\n')
376 382
377 383 def restart_kernel(self, message, instant_death=False):
378 384 """ Attempts to restart the running kernel.
379 385 """
380 386 # FIXME: instant_death should be configurable via a checkbox in the
381 387 # dialog. Right now at least the heartbeat path sets it to True and
382 388 # the manual restart to False. But those should just be the
383 389 # pre-selected states of a checkbox that the user could override if so
384 390 # desired. But I don't know enough Qt to go implementing the checkbox
385 391 # now.
386 392
387 393 # We want to make sure that if this dialog is already happening, that
388 394 # other signals don't trigger it again. This can happen when the
389 395 # kernel_died heartbeat signal is emitted and the user is slow to
390 396 # respond to the dialog.
391 397 if not self._possible_kernel_restart:
392 398 if self.custom_restart:
393 399 self.custom_restart_requested.emit()
394 400 elif self.kernel_manager.has_kernel:
395 401 # Setting this to True will prevent this logic from happening
396 402 # again until the current pass is completed.
397 403 self._possible_kernel_restart = True
398 404 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
399 405 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
400 406 message, buttons)
401 407 if result == QtGui.QMessageBox.Yes:
402 408 try:
403 409 self.kernel_manager.restart_kernel(
404 410 instant_death=instant_death)
405 411 except RuntimeError:
406 412 message = 'Kernel started externally. Cannot restart.\n'
407 413 self._append_plain_text(message)
408 414 else:
409 415 self._stopped_channels()
410 416 self._append_plain_text('Kernel restarting...\n')
411 417 self._show_interpreter_prompt()
412 418 # This might need to be moved to another location?
413 419 self._possible_kernel_restart = False
414 420 else:
415 421 self._append_plain_text('Kernel process is either remote or '
416 422 'unspecified. Cannot restart.\n')
417 423
418 424 #---------------------------------------------------------------------------
419 425 # 'FrontendWidget' protected interface
420 426 #---------------------------------------------------------------------------
421 427
422 428 def _call_tip(self):
423 429 """ Shows a call tip, if appropriate, at the current cursor location.
424 430 """
425 431 # Decide if it makes sense to show a call tip
426 432 cursor = self._get_cursor()
427 433 cursor.movePosition(QtGui.QTextCursor.Left)
428 434 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
429 435 return False
430 436 context = self._get_context(cursor)
431 437 if not context:
432 438 return False
433 439
434 440 # Send the metadata request to the kernel
435 441 name = '.'.join(context)
436 442 msg_id = self.kernel_manager.xreq_channel.object_info(name)
437 443 pos = self._get_cursor().position()
438 444 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
439 445 return True
440 446
441 447 def _complete(self):
442 448 """ Performs completion at the current cursor location.
443 449 """
444 450 context = self._get_context()
445 451 if context:
446 452 # Send the completion request to the kernel
447 453 msg_id = self.kernel_manager.xreq_channel.complete(
448 454 '.'.join(context), # text
449 455 self._get_input_buffer_cursor_line(), # line
450 456 self._get_input_buffer_cursor_column(), # cursor_pos
451 457 self.input_buffer) # block
452 458 pos = self._get_cursor().position()
453 459 info = self._CompletionRequest(msg_id, pos)
454 460 self._request_info['complete'] = info
455 461
456 462 def _get_banner(self):
457 463 """ Gets a banner to display at the beginning of a session.
458 464 """
459 465 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
460 466 '"license" for more information.'
461 467 return banner % (sys.version, sys.platform)
462 468
463 469 def _get_context(self, cursor=None):
464 470 """ Gets the context for the specified cursor (or the current cursor
465 471 if none is specified).
466 472 """
467 473 if cursor is None:
468 474 cursor = self._get_cursor()
469 475 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
470 476 QtGui.QTextCursor.KeepAnchor)
471 477 text = str(cursor.selection().toPlainText())
472 478 return self._completion_lexer.get_context(text)
473 479
474 480 def _process_execute_abort(self, msg):
475 481 """ Process a reply for an aborted execution request.
476 482 """
477 483 self._append_plain_text("ERROR: execution aborted\n")
478 484
479 485 def _process_execute_error(self, msg):
480 486 """ Process a reply for an execution request that resulted in an error.
481 487 """
482 488 content = msg['content']
483 489 traceback = ''.join(content['traceback'])
484 490 self._append_plain_text(traceback)
485 491
486 492 def _process_execute_ok(self, msg):
487 493 """ Process a reply for a successful execution equest.
488 494 """
489 495 payload = msg['content']['payload']
490 496 for item in payload:
491 497 if not self._process_execute_payload(item):
492 498 warning = 'Warning: received unknown payload of type %s'
493 499 raw_print(warning % repr(item['source']))
494 500
495 501 def _process_execute_payload(self, item):
496 502 """ Process a single payload item from the list of payload items in an
497 503 execution reply. Returns whether the payload was handled.
498 504 """
499 505 # The basic FrontendWidget doesn't handle payloads, as they are a
500 506 # mechanism for going beyond the standard Python interpreter model.
501 507 return False
502 508
503 509 def _show_interpreter_prompt(self):
504 510 """ Shows a prompt for the interpreter.
505 511 """
506 512 self._show_prompt('>>> ')
507 513
508 514 def _show_interpreter_prompt_for_reply(self, msg):
509 515 """ Shows a prompt for the interpreter given an 'execute_reply' message.
510 516 """
511 517 self._show_interpreter_prompt()
512 518
513 519 #------ Signal handlers ----------------------------------------------------
514 520
515 521 def _document_contents_change(self, position, removed, added):
516 522 """ Called whenever the document's content changes. Display a call tip
517 523 if appropriate.
518 524 """
519 525 # Calculate where the cursor should be *after* the change:
520 526 position += added
521 527
522 528 document = self._control.document()
523 529 if position == self._get_cursor().position():
524 530 self._call_tip()
General Comments 0
You need to be logged in to leave comments. Login now