##// END OF EJS Templates
* ConsoleWidget now has better support for non-GUI tab completion. Multiple matches are formatted into columns....
epatters -
Show More
@@ -0,0 +1,160 b''
1 """Return compact set of columns as a string with newlines for an
2 array of strings.
3
4 Adapted from the routine of the same name inside cmd.py
5
6 Author: Rocky Bernstein.
7 License: MIT Open Source License.
8 """
9
10 import types
11
12 def columnize(array, displaywidth=80, colsep = ' ',
13 arrange_vertical=True, ljust=True, lineprefix=''):
14 """Return a list of strings as a compact set of columns arranged
15 horizontally or vertically.
16
17 For example, for a line width of 4 characters (arranged vertically):
18 ['1', '2,', '3', '4'] => '1 3\n2 4\n'
19
20 or arranged horizontally:
21 ['1', '2,', '3', '4'] => '1 2\n3 4\n'
22
23 Each column is only as wide as necessary. By default, columns are
24 separated by two spaces - one was not legible enough. Set "colsep"
25 to adjust the string separate columns. Set `displaywidth' to set
26 the line width.
27
28 Normally, consecutive items go down from the top to bottom from
29 the left-most column to the right-most. If "arrange_vertical" is
30 set false, consecutive items will go across, left to right, top to
31 bottom."""
32 if not isinstance(array, list) and not isinstance(array, tuple):
33 raise TypeError, (
34 'array needs to be an instance of a list or a tuple')
35
36 array = [str(i) for i in array]
37
38 # Some degenerate cases
39 size = len(array)
40 if 0 == size:
41 return "<empty>\n"
42 elif size == 1:
43 return '%s\n' % str(array[0])
44
45 displaywidth = max(4, displaywidth - len(lineprefix))
46 if arrange_vertical:
47 array_index = lambda nrows, row, col: nrows*col + row
48 # Try every row count from 1 upwards
49 for nrows in range(1, size):
50 ncols = (size+nrows-1) // nrows
51 colwidths = []
52 totwidth = -len(colsep)
53 for col in range(ncols):
54 # get max column width for this column
55 colwidth = 0
56 for row in range(nrows):
57 i = array_index(nrows, row, col)
58 if i >= size: break
59 x = array[i]
60 colwidth = max(colwidth, len(x))
61 pass
62 colwidths.append(colwidth)
63 totwidth += colwidth + len(colsep)
64 if totwidth > displaywidth:
65 break
66 pass
67 if totwidth <= displaywidth:
68 break
69 pass
70 # The smallest number of rows computed and the
71 # max widths for each column has been obtained.
72 # Now we just have to format each of the
73 # rows.
74 s = ''
75 for row in range(nrows):
76 texts = []
77 for col in range(ncols):
78 i = row + nrows*col
79 if i >= size:
80 x = ""
81 else:
82 x = array[i]
83 texts.append(x)
84 while texts and not texts[-1]:
85 del texts[-1]
86 for col in range(len(texts)):
87 if ljust:
88 texts[col] = texts[col].ljust(colwidths[col])
89 else:
90 texts[col] = texts[col].rjust(colwidths[col])
91 pass
92 pass
93 s += "%s%s\n" % (lineprefix, str(colsep.join(texts)))
94 pass
95 return s
96 else:
97 array_index = lambda nrows, row, col: ncols*(row-1) + col
98 # Try every column count from size downwards
99 prev_colwidths = []
100 colwidths = []
101 for ncols in range(size, 0, -1):
102 # Try every row count from 1 upwards
103 min_rows = (size+ncols-1) // ncols
104 for nrows in range(min_rows, size):
105 rounded_size = nrows * ncols
106 colwidths = []
107 totwidth = -len(colsep)
108 for col in range(ncols):
109 # get max column width for this column
110 colwidth = 0
111 for row in range(1, nrows+1):
112 i = array_index(nrows, row, col)
113 if i >= rounded_size: break
114 elif i < size:
115 x = array[i]
116 colwidth = max(colwidth, len(x))
117 pass
118 pass
119 colwidths.append(colwidth)
120 totwidth += colwidth + len(colsep)
121 if totwidth >= displaywidth:
122 break
123 pass
124 if totwidth <= displaywidth and i >= rounded_size-1:
125 # Found the right nrows and ncols
126 nrows = row
127 break
128 elif totwidth >= displaywidth:
129 # Need to reduce ncols
130 break
131 pass
132 if totwidth <= displaywidth and i >= rounded_size-1:
133 break
134 pass
135 # The smallest number of rows computed and the
136 # max widths for each column has been obtained.
137 # Now we just have to format each of the
138 # rows.
139 s = ''
140 for row in range(1, nrows+1):
141 texts = []
142 for col in range(ncols):
143 i = array_index(nrows, row, col)
144 if i >= size:
145 break
146 else: x = array[i]
147 texts.append(x)
148 pass
149 for col in range(len(texts)):
150 if ljust:
151 texts[col] = texts[col].ljust(colwidths[col])
152 else:
153 texts[col] = texts[col].rjust(colwidths[col])
154 pass
155 pass
156 s += "%s%s\n" % (lineprefix, str(colsep.join(texts)))
157 pass
158 return s
159 pass
160
@@ -1,892 +1,931 b''
1 1 # Standard library imports
2 2 import sys
3 3
4 4 # System library imports
5 5 from PyQt4 import QtCore, QtGui
6 6
7 7 # Local imports
8 from IPython.external.columnize import columnize
8 9 from ansi_code_processor import QtAnsiCodeProcessor
9 10 from completion_widget import CompletionWidget
10 11
11 12
12 13 class ConsoleWidget(QtGui.QPlainTextEdit):
13 14 """ Base class for console-type widgets. This class is mainly concerned with
14 15 dealing with the prompt, keeping the cursor inside the editing line, and
15 16 handling ANSI escape sequences.
16 17 """
17 18
18 19 # Whether to process ANSI escape codes.
19 20 ansi_codes = True
20 21
21 22 # The maximum number of lines of text before truncation.
22 23 buffer_size = 500
23 24
24 25 # Whether to use a CompletionWidget or plain text output for tab completion.
25 26 gui_completion = True
26 27
27 28 # Whether to override ShortcutEvents for the keybindings defined by this
28 29 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
29 30 # priority (when it has focus) over, e.g., window-level menu shortcuts.
30 31 override_shortcuts = False
31 32
32 # The number of spaces to show for a tab character.
33 tab_width = 8
34
35 33 # Protected class variables.
36 34 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
37 35 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
38 36 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
39 37 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
40 38 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
41 39 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
42 40 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
43 41 _shortcuts = set(_ctrl_down_remap.keys() +
44 42 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
45 43
46 44 #---------------------------------------------------------------------------
47 45 # 'QObject' interface
48 46 #---------------------------------------------------------------------------
49 47
50 48 def __init__(self, parent=None):
51 49 QtGui.QPlainTextEdit.__init__(self, parent)
52 50
53 51 # Initialize protected variables. Some variables contain useful state
54 52 # information for subclasses; they should be considered read-only.
55 53 self._ansi_processor = QtAnsiCodeProcessor()
56 54 self._completion_widget = CompletionWidget(self)
57 55 self._continuation_prompt = '> '
58 56 self._continuation_prompt_html = None
59 57 self._executing = False
60 58 self._prompt = ''
61 59 self._prompt_html = None
62 60 self._prompt_pos = 0
63 61 self._reading = False
64 62 self._reading_callback = None
63 self._tab_width = 8
65 64
66 65 # Set a monospaced font.
67 66 self.reset_font()
68 67
69 68 # Define a custom context menu.
70 69 self._context_menu = QtGui.QMenu(self)
71 70
72 71 copy_action = QtGui.QAction('Copy', self)
73 72 copy_action.triggered.connect(self.copy)
74 73 self.copyAvailable.connect(copy_action.setEnabled)
75 74 self._context_menu.addAction(copy_action)
76 75
77 76 self._paste_action = QtGui.QAction('Paste', self)
78 77 self._paste_action.triggered.connect(self.paste)
79 78 self._context_menu.addAction(self._paste_action)
80 79 self._context_menu.addSeparator()
81 80
82 81 select_all_action = QtGui.QAction('Select All', self)
83 82 select_all_action.triggered.connect(self.selectAll)
84 83 self._context_menu.addAction(select_all_action)
85 84
86 85 def event(self, event):
87 86 """ Reimplemented to override shortcuts, if necessary.
88 87 """
89 88 # On Mac OS, it is always unnecessary to override shortcuts, hence the
90 89 # check below. Users should just use the Control key instead of the
91 90 # Command key.
92 91 if self.override_shortcuts and \
93 92 sys.platform != 'darwin' and \
94 93 event.type() == QtCore.QEvent.ShortcutOverride and \
95 94 self._control_down(event.modifiers()) and \
96 95 event.key() in self._shortcuts:
97 96 event.accept()
98 97 return True
99 98 else:
100 99 return QtGui.QPlainTextEdit.event(self, event)
101 100
102 101 #---------------------------------------------------------------------------
103 102 # 'QWidget' interface
104 103 #---------------------------------------------------------------------------
105 104
106 105 def contextMenuEvent(self, event):
107 106 """ Reimplemented to create a menu without destructive actions like
108 107 'Cut' and 'Delete'.
109 108 """
110 109 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
111 110 self._paste_action.setEnabled(not clipboard_empty)
112 111
113 112 self._context_menu.exec_(event.globalPos())
114 113
115 114 def dragMoveEvent(self, event):
116 """ Reimplemented to disable dropping text.
115 """ Reimplemented to disable moving text by drag and drop.
117 116 """
118 117 event.ignore()
119 118
120 119 def keyPressEvent(self, event):
121 120 """ Reimplemented to create a console-like interface.
122 121 """
123 122 intercepted = False
124 123 cursor = self.textCursor()
125 124 position = cursor.position()
126 125 key = event.key()
127 126 ctrl_down = self._control_down(event.modifiers())
128 127 alt_down = event.modifiers() & QtCore.Qt.AltModifier
129 128 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
130 129
131 130 # Even though we have reimplemented 'paste', the C++ level slot is still
132 131 # called by Qt. So we intercept the key press here.
133 132 if event.matches(QtGui.QKeySequence.Paste):
134 133 self.paste()
135 134 intercepted = True
136 135
137 136 elif ctrl_down:
138 137 if key in self._ctrl_down_remap:
139 138 ctrl_down = False
140 139 key = self._ctrl_down_remap[key]
141 140 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
142 141 QtCore.Qt.NoModifier)
143 142
144 143 elif key == QtCore.Qt.Key_K:
145 144 if self._in_buffer(position):
146 145 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
147 146 QtGui.QTextCursor.KeepAnchor)
148 147 cursor.removeSelectedText()
149 148 intercepted = True
150 149
151 150 elif key == QtCore.Qt.Key_X:
152 151 intercepted = True
153 152
154 153 elif key == QtCore.Qt.Key_Y:
155 154 self.paste()
156 155 intercepted = True
157 156
158 157 elif alt_down:
159 158 if key == QtCore.Qt.Key_B:
160 159 self.setTextCursor(self._get_word_start_cursor(position))
161 160 intercepted = True
162 161
163 162 elif key == QtCore.Qt.Key_F:
164 163 self.setTextCursor(self._get_word_end_cursor(position))
165 164 intercepted = True
166 165
167 166 elif key == QtCore.Qt.Key_Backspace:
168 167 cursor = self._get_word_start_cursor(position)
169 168 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
170 169 cursor.removeSelectedText()
171 170 intercepted = True
172 171
173 172 elif key == QtCore.Qt.Key_D:
174 173 cursor = self._get_word_end_cursor(position)
175 174 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
176 175 cursor.removeSelectedText()
177 176 intercepted = True
178 177
179 178 if self._completion_widget.isVisible():
180 179 self._completion_widget.keyPressEvent(event)
181 180 intercepted = event.isAccepted()
182 181
183 182 else:
184 183 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
185 184 if self._reading:
186 185 self.appendPlainText('\n')
187 186 self._reading = False
188 187 if self._reading_callback:
189 188 self._reading_callback()
190 189 elif not self._executing:
191 190 self.execute(interactive=True)
192 191 intercepted = True
193 192
194 193 elif key == QtCore.Qt.Key_Up:
195 194 if self._reading or not self._up_pressed():
196 195 intercepted = True
197 196 else:
198 197 prompt_line = self._get_prompt_cursor().blockNumber()
199 198 intercepted = cursor.blockNumber() <= prompt_line
200 199
201 200 elif key == QtCore.Qt.Key_Down:
202 201 if self._reading or not self._down_pressed():
203 202 intercepted = True
204 203 else:
205 204 end_line = self._get_end_cursor().blockNumber()
206 205 intercepted = cursor.blockNumber() == end_line
207 206
208 207 elif key == QtCore.Qt.Key_Tab:
209 208 if self._reading:
210 209 intercepted = False
211 210 else:
212 211 intercepted = not self._tab_pressed()
213 212
214 213 elif key == QtCore.Qt.Key_Left:
215 214 intercepted = not self._in_buffer(position - 1)
216 215
217 216 elif key == QtCore.Qt.Key_Home:
218 217 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
219 218 start_pos = cursor.position()
220 219 start_line = cursor.blockNumber()
221 220 if start_line == self._get_prompt_cursor().blockNumber():
222 221 start_pos += len(self._prompt)
223 222 else:
224 223 start_pos += len(self._continuation_prompt)
225 224 if shift_down and self._in_buffer(position):
226 225 self._set_selection(position, start_pos)
227 226 else:
228 227 self._set_position(start_pos)
229 228 intercepted = True
230 229
231 230 elif key == QtCore.Qt.Key_Backspace and not alt_down:
232 231
233 232 # Line deletion (remove continuation prompt)
234 233 len_prompt = len(self._continuation_prompt)
235 234 if not self._reading and \
236 235 cursor.columnNumber() == len_prompt and \
237 236 position != self._prompt_pos:
238 237 cursor.setPosition(position - len_prompt,
239 238 QtGui.QTextCursor.KeepAnchor)
240 239 cursor.removeSelectedText()
241 240
242 241 # Regular backwards deletion
243 242 else:
244 243 anchor = cursor.anchor()
245 244 if anchor == position:
246 245 intercepted = not self._in_buffer(position - 1)
247 246 else:
248 247 intercepted = not self._in_buffer(min(anchor, position))
249 248
250 249 elif key == QtCore.Qt.Key_Delete:
251 250 anchor = cursor.anchor()
252 251 intercepted = not self._in_buffer(min(anchor, position))
253 252
254 253 # Don't move cursor if control is down to allow copy-paste using
255 254 # the keyboard in any part of the buffer.
256 255 if not ctrl_down:
257 256 self._keep_cursor_in_buffer()
258 257
259 258 if not intercepted:
260 259 QtGui.QPlainTextEdit.keyPressEvent(self, event)
261 260
262 261 #--------------------------------------------------------------------------
263 262 # 'QPlainTextEdit' interface
264 263 #--------------------------------------------------------------------------
265 264
266 265 def appendHtml(self, html):
267 266 """ Reimplemented to not append HTML as a new paragraph, which doesn't
268 267 make sense for a console widget.
269 268 """
270 269 cursor = self._get_end_cursor()
271 270 cursor.insertHtml(html)
272 271
273 272 # After appending HTML, the text document "remembers" the current
274 273 # formatting, which means that subsequent calls to 'appendPlainText'
275 274 # will be formatted similarly, a behavior that we do not want. To
276 275 # prevent this, we make sure that the last character has no formatting.
277 276 cursor.movePosition(QtGui.QTextCursor.Left,
278 277 QtGui.QTextCursor.KeepAnchor)
279 278 if cursor.selection().toPlainText().trimmed().isEmpty():
280 279 # If the last character is whitespace, it doesn't matter how it's
281 280 # formatted, so just clear the formatting.
282 281 cursor.setCharFormat(QtGui.QTextCharFormat())
283 282 else:
284 283 # Otherwise, add an unformatted space.
285 284 cursor.movePosition(QtGui.QTextCursor.Right)
286 285 cursor.insertText(' ', QtGui.QTextCharFormat())
287 286
288 287 def appendPlainText(self, text):
289 288 """ Reimplemented to not append text as a new paragraph, which doesn't
290 289 make sense for a console widget. Also, if enabled, handle ANSI
291 290 codes.
292 291 """
293 292 cursor = self._get_end_cursor()
294 293 if self.ansi_codes:
295 294 for substring in self._ansi_processor.split_string(text):
296 295 format = self._ansi_processor.get_format()
297 296 cursor.insertText(substring, format)
298 297 else:
299 298 cursor.insertText(text)
300 299
301 300 def clear(self, keep_input=False):
302 301 """ Reimplemented to write a new prompt. If 'keep_input' is set,
303 302 restores the old input buffer when the new prompt is written.
304 303 """
305 304 QtGui.QPlainTextEdit.clear(self)
306 305 if keep_input:
307 306 input_buffer = self.input_buffer
308 307 self._show_prompt()
309 308 if keep_input:
310 309 self.input_buffer = input_buffer
311 310
312 311 def paste(self):
313 312 """ Reimplemented to ensure that text is pasted in the editing region.
314 313 """
315 314 self._keep_cursor_in_buffer()
316 315 QtGui.QPlainTextEdit.paste(self)
317 316
318 317 def print_(self, printer):
319 318 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
320 319 slot has the wrong signature.
321 320 """
322 321 QtGui.QPlainTextEdit.print_(self, printer)
323 322
324 323 #---------------------------------------------------------------------------
325 324 # 'ConsoleWidget' public interface
326 325 #---------------------------------------------------------------------------
327 326
328 327 def execute(self, source=None, hidden=False, interactive=False):
329 328 """ Executes source or the input buffer, possibly prompting for more
330 329 input.
331 330
332 331 Parameters:
333 332 -----------
334 333 source : str, optional
335 334
336 335 The source to execute. If not specified, the input buffer will be
337 336 used. If specified and 'hidden' is False, the input buffer will be
338 337 replaced with the source before execution.
339 338
340 339 hidden : bool, optional (default False)
341 340
342 341 If set, no output will be shown and the prompt will not be modified.
343 342 In other words, it will be completely invisible to the user that
344 343 an execution has occurred.
345 344
346 345 interactive : bool, optional (default False)
347 346
348 347 Whether the console is to treat the source as having been manually
349 348 entered by the user. The effect of this parameter depends on the
350 349 subclass implementation.
351 350
352 351 Raises:
353 352 -------
354 353 RuntimeError
355 354 If incomplete input is given and 'hidden' is True. In this case,
356 355 it not possible to prompt for more input.
357 356
358 357 Returns:
359 358 --------
360 359 A boolean indicating whether the source was executed.
361 360 """
362 361 if not hidden:
363 362 if source is not None:
364 363 self.input_buffer = source
365 364
366 365 self.appendPlainText('\n')
367 366 self._executing_input_buffer = self.input_buffer
368 367 self._executing = True
369 368 self._prompt_finished()
370 369
371 370 real_source = self.input_buffer if source is None else source
372 371 complete = self._is_complete(real_source, interactive)
373 372 if complete:
374 373 if not hidden:
375 374 # The maximum block count is only in effect during execution.
376 375 # This ensures that _prompt_pos does not become invalid due to
377 376 # text truncation.
378 377 self.setMaximumBlockCount(self.buffer_size)
379 378 self._execute(real_source, hidden)
380 379 elif hidden:
381 380 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
382 381 else:
383 382 self._show_continuation_prompt()
384 383
385 384 return complete
386 385
387 386 def _get_input_buffer(self):
388 387 """ The text that the user has entered entered at the current prompt.
389 388 """
390 389 # If we're executing, the input buffer may not even exist anymore due to
391 390 # the limit imposed by 'buffer_size'. Therefore, we store it.
392 391 if self._executing:
393 392 return self._executing_input_buffer
394 393
395 394 cursor = self._get_end_cursor()
396 395 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
397 396 input_buffer = str(cursor.selection().toPlainText())
398 397
399 398 # Strip out continuation prompts.
400 399 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
401 400
402 401 def _set_input_buffer(self, string):
403 402 """ Replaces the text in the input buffer with 'string'.
404 403 """
405 404 # Remove old text.
406 405 cursor = self._get_end_cursor()
407 406 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
408 407 cursor.removeSelectedText()
409 408
410 409 # Insert new text with continuation prompts.
411 410 lines = string.splitlines(True)
412 411 if lines:
413 412 self.appendPlainText(lines[0])
414 413 for i in xrange(1, len(lines)):
415 414 if self._continuation_prompt_html is None:
416 415 self.appendPlainText(self._continuation_prompt)
417 416 else:
418 417 self.appendHtml(self._continuation_prompt_html)
419 418 self.appendPlainText(lines[i])
420 419 self.moveCursor(QtGui.QTextCursor.End)
421 420
422 421 input_buffer = property(_get_input_buffer, _set_input_buffer)
423 422
424 423 def _get_input_buffer_cursor_line(self):
425 424 """ The text in the line of the input buffer in which the user's cursor
426 425 rests. Returns a string if there is such a line; otherwise, None.
427 426 """
428 427 if self._executing:
429 428 return None
430 429 cursor = self.textCursor()
431 430 if cursor.position() >= self._prompt_pos:
432 431 text = self._get_block_plain_text(cursor.block())
433 432 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
434 433 return text[len(self._prompt):]
435 434 else:
436 435 return text[len(self._continuation_prompt):]
437 436 else:
438 437 return None
439 438
440 439 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
441 440
442 441 def _get_font(self):
443 442 """ The base font being used by the ConsoleWidget.
444 443 """
445 444 return self.document().defaultFont()
446 445
447 446 def _set_font(self, font):
448 447 """ Sets the base font for the ConsoleWidget to the specified QFont.
449 448 """
450 449 font_metrics = QtGui.QFontMetrics(font)
451 450 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
452 451
453 452 self._completion_widget.setFont(font)
454 453 self.document().setDefaultFont(font)
455 454
456 455 font = property(_get_font, _set_font)
457 456
458 457 def reset_font(self):
459 458 """ Sets the font to the default fixed-width font for this platform.
460 459 """
461 460 if sys.platform == 'win32':
462 461 name = 'Courier'
463 462 elif sys.platform == 'darwin':
464 463 name = 'Monaco'
465 464 else:
466 465 name = 'Monospace'
467 466 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
468 467 font.setStyleHint(QtGui.QFont.TypeWriter)
469 468 self._set_font(font)
469
470 def _get_tab_width(self):
471 """ The width (in terms of space characters) for tab characters.
472 """
473 return self._tab_width
474
475 def _set_tab_width(self, tab_width):
476 """ Sets the width (in terms of space characters) for tab characters.
477 """
478 font_metrics = QtGui.QFontMetrics(self.font)
479 self.setTabStopWidth(tab_width * font_metrics.width(' '))
480
481 self._tab_width = tab_width
482
483 tab_width = property(_get_tab_width, _set_tab_width)
470 484
471 485 #---------------------------------------------------------------------------
472 486 # 'ConsoleWidget' abstract interface
473 487 #---------------------------------------------------------------------------
474 488
475 489 def _is_complete(self, source, interactive):
476 490 """ Returns whether 'source' can be executed. When triggered by an
477 491 Enter/Return key press, 'interactive' is True; otherwise, it is
478 492 False.
479 493 """
480 494 raise NotImplementedError
481 495
482 496 def _execute(self, source, hidden):
483 497 """ Execute 'source'. If 'hidden', do not show any output.
484 498 """
485 499 raise NotImplementedError
486 500
487 501 def _prompt_started_hook(self):
488 502 """ Called immediately after a new prompt is displayed.
489 503 """
490 504 pass
491 505
492 506 def _prompt_finished_hook(self):
493 507 """ Called immediately after a prompt is finished, i.e. when some input
494 508 will be processed and a new prompt displayed.
495 509 """
496 510 pass
497 511
498 512 def _up_pressed(self):
499 513 """ Called when the up key is pressed. Returns whether to continue
500 514 processing the event.
501 515 """
502 516 return True
503 517
504 518 def _down_pressed(self):
505 519 """ Called when the down key is pressed. Returns whether to continue
506 520 processing the event.
507 521 """
508 522 return True
509 523
510 524 def _tab_pressed(self):
511 525 """ Called when the tab key is pressed. Returns whether to continue
512 526 processing the event.
513 527 """
514 528 return False
515 529
516 530 #--------------------------------------------------------------------------
517 531 # 'ConsoleWidget' protected interface
518 532 #--------------------------------------------------------------------------
519 533
520 534 def _append_html_fetching_plain_text(self, html):
521 535 """ Appends 'html', then returns the plain text version of it.
522 536 """
523 537 anchor = self._get_end_cursor().position()
524 538 self.appendHtml(html)
525 539 cursor = self._get_end_cursor()
526 540 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
527 541 return str(cursor.selection().toPlainText())
528 542
529 543 def _append_plain_text_keeping_prompt(self, text):
530 544 """ Writes 'text' after the current prompt, then restores the old prompt
531 545 with its old input buffer.
532 546 """
533 547 input_buffer = self.input_buffer
534 548 self.appendPlainText('\n')
535 549 self._prompt_finished()
536 550
537 551 self.appendPlainText(text)
538 552 self._show_prompt()
539 553 self.input_buffer = input_buffer
540 554
541 555 def _control_down(self, modifiers):
542 556 """ Given a KeyboardModifiers flags object, return whether the Control
543 557 key is down (on Mac OS, treat the Command key as a synonym for
544 558 Control).
545 559 """
546 560 down = bool(modifiers & QtCore.Qt.ControlModifier)
547 561
548 562 # Note: on Mac OS, ControlModifier corresponds to the Command key while
549 563 # MetaModifier corresponds to the Control key.
550 564 if sys.platform == 'darwin':
551 565 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
552 566
553 567 return down
554 568
555 569 def _complete_with_items(self, cursor, items):
556 570 """ Performs completion with 'items' at the specified cursor location.
557 571 """
558 572 if len(items) == 1:
559 573 cursor.setPosition(self.textCursor().position(),
560 574 QtGui.QTextCursor.KeepAnchor)
561 575 cursor.insertText(items[0])
562 576 elif len(items) > 1:
563 577 if self.gui_completion:
564 578 self._completion_widget.show_items(cursor, items)
565 579 else:
566 text = '\n'.join(items) + '\n'
580 text = self.format_as_columns(items)
567 581 self._append_plain_text_keeping_prompt(text)
568 582
583 def format_as_columns(self, items, separator=' ', vertical=True):
584 """ Transform a list of strings into a single string with columns.
585
586 Parameters
587 ----------
588 items : sequence [str]
589 The strings to process.
590
591 separator : str, optional [default is two spaces]
592 The string that separates columns.
593
594 vertical: bool, optional [default True]
595 If set, consecutive items will be arranged from top to bottom, then
596 from left to right. Otherwise, consecutive items will be aranged
597 from left to right, then from top to bottom.
598
599 Returns
600 -------
601 The formatted string.
602 """
603 font_metrics = QtGui.QFontMetrics(self.font)
604 width = self.width() / font_metrics.width(' ')
605 return columnize(items, displaywidth=width,
606 colsep=separator, arrange_vertical=vertical)
607
569 608 def _get_block_plain_text(self, block):
570 609 """ Given a QTextBlock, return its unformatted text.
571 610 """
572 611 cursor = QtGui.QTextCursor(block)
573 612 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
574 613 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
575 614 QtGui.QTextCursor.KeepAnchor)
576 615 return str(cursor.selection().toPlainText())
577 616
578 617 def _get_end_cursor(self):
579 618 """ Convenience method that returns a cursor for the last character.
580 619 """
581 620 cursor = self.textCursor()
582 621 cursor.movePosition(QtGui.QTextCursor.End)
583 622 return cursor
584 623
585 624 def _get_prompt_cursor(self):
586 625 """ Convenience method that returns a cursor for the prompt position.
587 626 """
588 627 cursor = self.textCursor()
589 628 cursor.setPosition(self._prompt_pos)
590 629 return cursor
591 630
592 631 def _get_selection_cursor(self, start, end):
593 632 """ Convenience method that returns a cursor with text selected between
594 633 the positions 'start' and 'end'.
595 634 """
596 635 cursor = self.textCursor()
597 636 cursor.setPosition(start)
598 637 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
599 638 return cursor
600 639
601 640 def _get_word_start_cursor(self, position):
602 641 """ Find the start of the word to the left the given position. If a
603 642 sequence of non-word characters precedes the first word, skip over
604 643 them. (This emulates the behavior of bash, emacs, etc.)
605 644 """
606 645 document = self.document()
607 646 position -= 1
608 647 while self._in_buffer(position) and \
609 648 not document.characterAt(position).isLetterOrNumber():
610 649 position -= 1
611 650 while self._in_buffer(position) and \
612 651 document.characterAt(position).isLetterOrNumber():
613 652 position -= 1
614 653 cursor = self.textCursor()
615 654 cursor.setPosition(position + 1)
616 655 return cursor
617 656
618 657 def _get_word_end_cursor(self, position):
619 658 """ Find the end of the word to the right the given position. If a
620 659 sequence of non-word characters precedes the first word, skip over
621 660 them. (This emulates the behavior of bash, emacs, etc.)
622 661 """
623 662 document = self.document()
624 663 end = self._get_end_cursor().position()
625 664 while position < end and \
626 665 not document.characterAt(position).isLetterOrNumber():
627 666 position += 1
628 667 while position < end and \
629 668 document.characterAt(position).isLetterOrNumber():
630 669 position += 1
631 670 cursor = self.textCursor()
632 671 cursor.setPosition(position)
633 672 return cursor
634 673
635 674 def _prompt_started(self):
636 675 """ Called immediately after a new prompt is displayed.
637 676 """
638 677 # Temporarily disable the maximum block count to permit undo/redo and
639 678 # to ensure that the prompt position does not change due to truncation.
640 679 self.setMaximumBlockCount(0)
641 680 self.setUndoRedoEnabled(True)
642 681
643 682 self.setReadOnly(False)
644 683 self.moveCursor(QtGui.QTextCursor.End)
645 684 self.centerCursor()
646 685
647 686 self._executing = False
648 687 self._prompt_started_hook()
649 688
650 689 def _prompt_finished(self):
651 690 """ Called immediately after a prompt is finished, i.e. when some input
652 691 will be processed and a new prompt displayed.
653 692 """
654 693 self.setUndoRedoEnabled(False)
655 694 self.setReadOnly(True)
656 695 self._prompt_finished_hook()
657 696
658 697 def _readline(self, prompt='', callback=None):
659 698 """ Reads one line of input from the user.
660 699
661 700 Parameters
662 701 ----------
663 702 prompt : str, optional
664 703 The prompt to print before reading the line.
665 704
666 705 callback : callable, optional
667 706 A callback to execute with the read line. If not specified, input is
668 707 read *synchronously* and this method does not return until it has
669 708 been read.
670 709
671 710 Returns
672 711 -------
673 712 If a callback is specified, returns nothing. Otherwise, returns the
674 713 input string with the trailing newline stripped.
675 714 """
676 715 if self._reading:
677 716 raise RuntimeError('Cannot read a line. Widget is already reading.')
678 717
679 718 if not callback and not self.isVisible():
680 719 # If the user cannot see the widget, this function cannot return.
681 720 raise RuntimeError('Cannot synchronously read a line if the widget'
682 721 'is not visible!')
683 722
684 723 self._reading = True
685 724 self._show_prompt(prompt, newline=False)
686 725
687 726 if callback is None:
688 727 self._reading_callback = None
689 728 while self._reading:
690 729 QtCore.QCoreApplication.processEvents()
691 730 return self.input_buffer.rstrip('\n')
692 731
693 732 else:
694 733 self._reading_callback = lambda: \
695 734 callback(self.input_buffer.rstrip('\n'))
696 735
697 736 def _reset(self):
698 737 """ Clears the console and resets internal state variables.
699 738 """
700 739 QtGui.QPlainTextEdit.clear(self)
701 740 self._executing = self._reading = False
702 741
703 742 def _set_continuation_prompt(self, prompt, html=False):
704 743 """ Sets the continuation prompt.
705 744
706 745 Parameters
707 746 ----------
708 747 prompt : str
709 748 The prompt to show when more input is needed.
710 749
711 750 html : bool, optional (default False)
712 751 If set, the prompt will be inserted as formatted HTML. Otherwise,
713 752 the prompt will be treated as plain text, though ANSI color codes
714 753 will be handled.
715 754 """
716 755 if html:
717 756 self._continuation_prompt_html = prompt
718 757 else:
719 758 self._continuation_prompt = prompt
720 759 self._continuation_prompt_html = None
721 760
722 761 def _set_position(self, position):
723 762 """ Convenience method to set the position of the cursor.
724 763 """
725 764 cursor = self.textCursor()
726 765 cursor.setPosition(position)
727 766 self.setTextCursor(cursor)
728 767
729 768 def _set_selection(self, start, end):
730 769 """ Convenience method to set the current selected text.
731 770 """
732 771 self.setTextCursor(self._get_selection_cursor(start, end))
733 772
734 773 def _show_prompt(self, prompt=None, html=False, newline=True):
735 774 """ Writes a new prompt at the end of the buffer.
736 775
737 776 Parameters
738 777 ----------
739 778 prompt : str, optional
740 779 The prompt to show. If not specified, the previous prompt is used.
741 780
742 781 html : bool, optional (default False)
743 782 Only relevant when a prompt is specified. If set, the prompt will
744 783 be inserted as formatted HTML. Otherwise, the prompt will be treated
745 784 as plain text, though ANSI color codes will be handled.
746 785
747 786 newline : bool, optional (default True)
748 787 If set, a new line will be written before showing the prompt if
749 788 there is not already a newline at the end of the buffer.
750 789 """
751 790 # Insert a preliminary newline, if necessary.
752 791 if newline:
753 792 cursor = self._get_end_cursor()
754 793 if cursor.position() > 0:
755 794 cursor.movePosition(QtGui.QTextCursor.Left,
756 795 QtGui.QTextCursor.KeepAnchor)
757 796 if str(cursor.selection().toPlainText()) != '\n':
758 797 self.appendPlainText('\n')
759 798
760 799 # Write the prompt.
761 800 if prompt is None:
762 801 if self._prompt_html is None:
763 802 self.appendPlainText(self._prompt)
764 803 else:
765 804 self.appendHtml(self._prompt_html)
766 805 else:
767 806 if html:
768 807 self._prompt = self._append_html_fetching_plain_text(prompt)
769 808 self._prompt_html = prompt
770 809 else:
771 810 self.appendPlainText(prompt)
772 811 self._prompt = prompt
773 812 self._prompt_html = None
774 813
775 814 self._prompt_pos = self._get_end_cursor().position()
776 815 self._prompt_started()
777 816
778 817 def _show_continuation_prompt(self):
779 818 """ Writes a new continuation prompt at the end of the buffer.
780 819 """
781 820 if self._continuation_prompt_html is None:
782 821 self.appendPlainText(self._continuation_prompt)
783 822 else:
784 823 self._continuation_prompt = self._append_html_fetching_plain_text(
785 824 self._continuation_prompt_html)
786 825
787 826 self._prompt_started()
788 827
789 828 def _in_buffer(self, position):
790 829 """ Returns whether the given position is inside the editing region.
791 830 """
792 831 return position >= self._prompt_pos
793 832
794 833 def _keep_cursor_in_buffer(self):
795 834 """ Ensures that the cursor is inside the editing region. Returns
796 835 whether the cursor was moved.
797 836 """
798 837 cursor = self.textCursor()
799 838 if cursor.position() < self._prompt_pos:
800 839 cursor.movePosition(QtGui.QTextCursor.End)
801 840 self.setTextCursor(cursor)
802 841 return True
803 842 else:
804 843 return False
805 844
806 845
807 846 class HistoryConsoleWidget(ConsoleWidget):
808 847 """ A ConsoleWidget that keeps a history of the commands that have been
809 848 executed.
810 849 """
811 850
812 851 #---------------------------------------------------------------------------
813 852 # 'QObject' interface
814 853 #---------------------------------------------------------------------------
815 854
816 855 def __init__(self, parent=None):
817 856 super(HistoryConsoleWidget, self).__init__(parent)
818 857
819 858 self._history = []
820 859 self._history_index = 0
821 860
822 861 #---------------------------------------------------------------------------
823 862 # 'ConsoleWidget' public interface
824 863 #---------------------------------------------------------------------------
825 864
826 865 def execute(self, source=None, hidden=False, interactive=False):
827 866 """ Reimplemented to the store history.
828 867 """
829 868 if not hidden:
830 869 history = self.input_buffer if source is None else source
831 870
832 871 executed = super(HistoryConsoleWidget, self).execute(
833 872 source, hidden, interactive)
834 873
835 874 if executed and not hidden:
836 875 self._history.append(history.rstrip())
837 876 self._history_index = len(self._history)
838 877
839 878 return executed
840 879
841 880 #---------------------------------------------------------------------------
842 881 # 'ConsoleWidget' abstract interface
843 882 #---------------------------------------------------------------------------
844 883
845 884 def _up_pressed(self):
846 885 """ Called when the up key is pressed. Returns whether to continue
847 886 processing the event.
848 887 """
849 888 prompt_cursor = self._get_prompt_cursor()
850 889 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
851 890 self.history_previous()
852 891
853 892 # Go to the first line of prompt for seemless history scrolling.
854 893 cursor = self._get_prompt_cursor()
855 894 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
856 895 self.setTextCursor(cursor)
857 896
858 897 return False
859 898 return True
860 899
861 900 def _down_pressed(self):
862 901 """ Called when the down key is pressed. Returns whether to continue
863 902 processing the event.
864 903 """
865 904 end_cursor = self._get_end_cursor()
866 905 if self.textCursor().blockNumber() == end_cursor.blockNumber():
867 906 self.history_next()
868 907 return False
869 908 return True
870 909
871 910 #---------------------------------------------------------------------------
872 911 # 'HistoryConsoleWidget' interface
873 912 #---------------------------------------------------------------------------
874 913
875 914 def history_previous(self):
876 915 """ If possible, set the input buffer to the previous item in the
877 916 history.
878 917 """
879 918 if self._history_index > 0:
880 919 self._history_index -= 1
881 920 self.input_buffer = self._history[self._history_index]
882 921
883 922 def history_next(self):
884 923 """ Set the input buffer to the next item in the history, or a blank
885 924 line if there is no subsequent item.
886 925 """
887 926 if self._history_index < len(self._history):
888 927 self._history_index += 1
889 928 if self._history_index < len(self._history):
890 929 self.input_buffer = self._history[self._history_index]
891 930 else:
892 931 self.input_buffer = ''
@@ -1,384 +1,382 b''
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8 import zmq
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 12 from call_tip_widget import CallTipWidget
13 13 from completion_lexer import CompletionLexer
14 14 from console_widget import HistoryConsoleWidget
15 15 from pygments_highlighter import PygmentsHighlighter
16 16
17 17
18 18 class FrontendHighlighter(PygmentsHighlighter):
19 19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 20 prompts.
21 21 """
22 22
23 23 def __init__(self, frontend):
24 24 super(FrontendHighlighter, self).__init__(frontend.document())
25 25 self._current_offset = 0
26 26 self._frontend = frontend
27 27 self.highlighting_on = False
28 28
29 29 def highlightBlock(self, qstring):
30 30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 31 """
32 32 if not self.highlighting_on:
33 33 return
34 34
35 35 # The input to this function is unicode string that may contain
36 36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 37 # the string as plain text so we can compare it.
38 38 current_block = self.currentBlock()
39 39 string = self._frontend._get_block_plain_text(current_block)
40 40
41 41 # Decide whether to check for the regular or continuation prompt.
42 42 if current_block.contains(self._frontend._prompt_pos):
43 43 prompt = self._frontend._prompt
44 44 else:
45 45 prompt = self._frontend._continuation_prompt
46 46
47 47 # Don't highlight the part of the string that contains the prompt.
48 48 if string.startswith(prompt):
49 49 self._current_offset = len(prompt)
50 50 qstring.remove(0, len(prompt))
51 51 else:
52 52 self._current_offset = 0
53 53
54 54 PygmentsHighlighter.highlightBlock(self, qstring)
55 55
56 56 def setFormat(self, start, count, format):
57 57 """ Reimplemented to highlight selectively.
58 58 """
59 59 start += self._current_offset
60 60 PygmentsHighlighter.setFormat(self, start, count, format)
61 61
62 62
63 63 class FrontendWidget(HistoryConsoleWidget):
64 64 """ A Qt frontend for a generic Python kernel.
65 65 """
66
67 # ConsoleWidget interface.
68 tab_width = 4
69
66
70 67 # Emitted when an 'execute_reply' is received from the kernel.
71 68 executed = QtCore.pyqtSignal(object)
72 69
73 70 #---------------------------------------------------------------------------
74 71 # 'QObject' interface
75 72 #---------------------------------------------------------------------------
76 73
77 74 def __init__(self, parent=None):
78 75 super(FrontendWidget, self).__init__(parent)
79 76
80 77 # FrontendWidget protected variables.
81 78 self._call_tip_widget = CallTipWidget(self)
82 79 self._completion_lexer = CompletionLexer(PythonLexer())
83 80 self._hidden = True
84 81 self._highlighter = FrontendHighlighter(self)
85 82 self._input_splitter = InputSplitter(input_mode='replace')
86 83 self._kernel_manager = None
87 84
88 85 # Configure the ConsoleWidget.
86 self.tab_width = 4
89 87 self._set_continuation_prompt('... ')
90 88
91 89 self.document().contentsChange.connect(self._document_contents_change)
92 90
93 91 #---------------------------------------------------------------------------
94 92 # 'QWidget' interface
95 93 #---------------------------------------------------------------------------
96 94
97 95 def focusOutEvent(self, event):
98 96 """ Reimplemented to hide calltips.
99 97 """
100 98 self._call_tip_widget.hide()
101 99 super(FrontendWidget, self).focusOutEvent(event)
102 100
103 101 def keyPressEvent(self, event):
104 102 """ Reimplemented to allow calltips to process events and to send
105 103 signals to the kernel.
106 104 """
107 105 if self._executing and event.key() == QtCore.Qt.Key_C and \
108 106 self._control_down(event.modifiers()):
109 107 self._interrupt_kernel()
110 108 else:
111 109 if self._call_tip_widget.isVisible():
112 110 self._call_tip_widget.keyPressEvent(event)
113 111 super(FrontendWidget, self).keyPressEvent(event)
114 112
115 113 #---------------------------------------------------------------------------
116 114 # 'ConsoleWidget' abstract interface
117 115 #---------------------------------------------------------------------------
118 116
119 117 def _is_complete(self, source, interactive):
120 118 """ Returns whether 'source' can be completely processed and a new
121 119 prompt created. When triggered by an Enter/Return key press,
122 120 'interactive' is True; otherwise, it is False.
123 121 """
124 complete = self._input_splitter.push(source.replace('\t', ' '))
122 complete = self._input_splitter.push(source.expandtabs(4))
125 123 if interactive:
126 124 complete = not self._input_splitter.push_accepts_more()
127 125 return complete
128 126
129 127 def _execute(self, source, hidden):
130 128 """ Execute 'source'. If 'hidden', do not show any output.
131 129 """
132 130 self.kernel_manager.xreq_channel.execute(source)
133 131 self._hidden = hidden
134 132
135 133 def _prompt_started_hook(self):
136 134 """ Called immediately after a new prompt is displayed.
137 135 """
138 136 if not self._reading:
139 137 self._highlighter.highlighting_on = True
140 138
141 139 # Auto-indent if this is a continuation prompt.
142 140 if self._get_prompt_cursor().blockNumber() != \
143 141 self._get_end_cursor().blockNumber():
144 142 spaces = self._input_splitter.indent_spaces
145 143 self.appendPlainText('\t' * (spaces / self.tab_width))
146 144 self.appendPlainText(' ' * (spaces % self.tab_width))
147 145
148 146 def _prompt_finished_hook(self):
149 147 """ Called immediately after a prompt is finished, i.e. when some input
150 148 will be processed and a new prompt displayed.
151 149 """
152 150 if not self._reading:
153 151 self._highlighter.highlighting_on = False
154 152
155 153 def _tab_pressed(self):
156 154 """ Called when the tab key is pressed. Returns whether to continue
157 155 processing the event.
158 156 """
159 157 self._keep_cursor_in_buffer()
160 158 cursor = self.textCursor()
161 159 return not self._complete()
162 160
163 161 #---------------------------------------------------------------------------
164 162 # 'FrontendWidget' interface
165 163 #---------------------------------------------------------------------------
166 164
167 165 def execute_file(self, path, hidden=False):
168 166 """ Attempts to execute file with 'path'. If 'hidden', no output is
169 167 shown.
170 168 """
171 169 self.execute('execfile("%s")' % path, hidden=hidden)
172 170
173 171 def _get_kernel_manager(self):
174 172 """ Returns the current kernel manager.
175 173 """
176 174 return self._kernel_manager
177 175
178 176 def _set_kernel_manager(self, kernel_manager):
179 177 """ Disconnect from the current kernel manager (if any) and set a new
180 178 kernel manager.
181 179 """
182 180 # Disconnect the old kernel manager, if necessary.
183 181 if self._kernel_manager is not None:
184 182 self._kernel_manager.started_channels.disconnect(
185 183 self._started_channels)
186 184 self._kernel_manager.stopped_channels.disconnect(
187 185 self._stopped_channels)
188 186
189 187 # Disconnect the old kernel manager's channels.
190 188 sub = self._kernel_manager.sub_channel
191 189 xreq = self._kernel_manager.xreq_channel
192 190 rep = self._kernel_manager.rep_channel
193 191 sub.message_received.disconnect(self._handle_sub)
194 192 xreq.execute_reply.disconnect(self._handle_execute_reply)
195 193 xreq.complete_reply.disconnect(self._handle_complete_reply)
196 194 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
197 195 rep.readline_requested.disconnect(self._handle_req)
198 196
199 197 # Handle the case where the old kernel manager is still listening.
200 198 if self._kernel_manager.channels_running:
201 199 self._stopped_channels()
202 200
203 201 # Set the new kernel manager.
204 202 self._kernel_manager = kernel_manager
205 203 if kernel_manager is None:
206 204 return
207 205
208 206 # Connect the new kernel manager.
209 207 kernel_manager.started_channels.connect(self._started_channels)
210 208 kernel_manager.stopped_channels.connect(self._stopped_channels)
211 209
212 210 # Connect the new kernel manager's channels.
213 211 sub = kernel_manager.sub_channel
214 212 xreq = kernel_manager.xreq_channel
215 213 rep = kernel_manager.rep_channel
216 214 sub.message_received.connect(self._handle_sub)
217 215 xreq.execute_reply.connect(self._handle_execute_reply)
218 216 xreq.complete_reply.connect(self._handle_complete_reply)
219 217 xreq.object_info_reply.connect(self._handle_object_info_reply)
220 218 rep.readline_requested.connect(self._handle_req)
221 219
222 220 # Handle the case where the kernel manager started channels before
223 221 # we connected.
224 222 if kernel_manager.channels_running:
225 223 self._started_channels()
226 224
227 225 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
228 226
229 227 #---------------------------------------------------------------------------
230 228 # 'FrontendWidget' protected interface
231 229 #---------------------------------------------------------------------------
232 230
233 231 def _call_tip(self):
234 232 """ Shows a call tip, if appropriate, at the current cursor location.
235 233 """
236 234 # Decide if it makes sense to show a call tip
237 235 cursor = self.textCursor()
238 236 cursor.movePosition(QtGui.QTextCursor.Left)
239 237 document = self.document()
240 238 if document.characterAt(cursor.position()).toAscii() != '(':
241 239 return False
242 240 context = self._get_context(cursor)
243 241 if not context:
244 242 return False
245 243
246 244 # Send the metadata request to the kernel
247 245 name = '.'.join(context)
248 246 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
249 247 self._calltip_pos = self.textCursor().position()
250 248 return True
251 249
252 250 def _complete(self):
253 251 """ Performs completion at the current cursor location.
254 252 """
255 253 # Decide if it makes sense to do completion
256 254 context = self._get_context()
257 255 if not context:
258 256 return False
259 257
260 258 # Send the completion request to the kernel
261 259 text = '.'.join(context)
262 260 self._complete_id = self.kernel_manager.xreq_channel.complete(
263 261 text, self.input_buffer_cursor_line, self.input_buffer)
264 262 self._complete_pos = self.textCursor().position()
265 263 return True
266 264
267 265 def _get_banner(self):
268 266 """ Gets a banner to display at the beginning of a session.
269 267 """
270 268 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
271 269 '"license" for more information.'
272 270 return banner % (sys.version, sys.platform)
273 271
274 272 def _get_context(self, cursor=None):
275 273 """ Gets the context at the current cursor location.
276 274 """
277 275 if cursor is None:
278 276 cursor = self.textCursor()
279 277 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
280 278 QtGui.QTextCursor.KeepAnchor)
281 279 text = str(cursor.selection().toPlainText())
282 280 return self._completion_lexer.get_context(text)
283 281
284 282 def _interrupt_kernel(self):
285 283 """ Attempts to the interrupt the kernel.
286 284 """
287 285 if self.kernel_manager.has_kernel:
288 286 self.kernel_manager.signal_kernel(signal.SIGINT)
289 287 else:
290 288 self.appendPlainText('Kernel process is either remote or '
291 289 'unspecified. Cannot interrupt.\n')
292 290
293 291 def _show_interpreter_prompt(self):
294 292 """ Shows a prompt for the interpreter.
295 293 """
296 294 self._show_prompt('>>> ')
297 295
298 296 #------ Signal handlers ----------------------------------------------------
299 297
300 298 def _started_channels(self):
301 299 """ Called when the kernel manager has started listening.
302 300 """
303 301 self._reset()
304 302 self.appendPlainText(self._get_banner())
305 303 self._show_interpreter_prompt()
306 304
307 305 def _stopped_channels(self):
308 306 """ Called when the kernel manager has stopped listening.
309 307 """
310 308 # FIXME: Print a message here?
311 309 pass
312 310
313 311 def _document_contents_change(self, position, removed, added):
314 312 """ Called whenever the document's content changes. Display a calltip
315 313 if appropriate.
316 314 """
317 315 # Calculate where the cursor should be *after* the change:
318 316 position += added
319 317
320 318 document = self.document()
321 319 if position == self.textCursor().position():
322 320 self._call_tip()
323 321
324 322 def _handle_req(self, req):
325 323 # Make sure that all output from the SUB channel has been processed
326 324 # before entering readline mode.
327 325 self.kernel_manager.sub_channel.flush()
328 326
329 327 def callback(line):
330 328 self.kernel_manager.rep_channel.readline(line)
331 329 self._readline(callback=callback)
332 330
333 331 def _handle_sub(self, omsg):
334 332 if self._hidden:
335 333 return
336 334 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
337 335 if handler is not None:
338 336 handler(omsg)
339 337
340 338 def _handle_pyout(self, omsg):
341 339 self.appendPlainText(omsg['content']['data'] + '\n')
342 340
343 341 def _handle_stream(self, omsg):
344 342 self.appendPlainText(omsg['content']['data'])
345 343 self.moveCursor(QtGui.QTextCursor.End)
346 344
347 345 def _handle_execute_reply(self, reply):
348 346 if self._hidden:
349 347 return
350 348
351 349 # Make sure that all output from the SUB channel has been processed
352 350 # before writing a new prompt.
353 351 self.kernel_manager.sub_channel.flush()
354 352
355 353 status = reply['content']['status']
356 354 if status == 'error':
357 355 self._handle_execute_error(reply)
358 356 elif status == 'aborted':
359 357 text = "ERROR: ABORTED\n"
360 358 self.appendPlainText(text)
361 359 self._hidden = True
362 360 self._show_interpreter_prompt()
363 361 self.executed.emit(reply)
364 362
365 363 def _handle_execute_error(self, reply):
366 364 content = reply['content']
367 365 traceback = ''.join(content['traceback'])
368 366 self.appendPlainText(traceback)
369 367
370 368 def _handle_complete_reply(self, rep):
371 369 cursor = self.textCursor()
372 370 if rep['parent_header']['msg_id'] == self._complete_id and \
373 371 cursor.position() == self._complete_pos:
374 372 text = '.'.join(self._get_context())
375 373 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
376 374 self._complete_with_items(cursor, rep['content']['matches'])
377 375
378 376 def _handle_object_info_reply(self, rep):
379 377 cursor = self.textCursor()
380 378 if rep['parent_header']['msg_id'] == self._calltip_id and \
381 379 cursor.position() == self._calltip_pos:
382 380 doc = rep['content']['docstring']
383 381 if doc:
384 382 self._call_tip_widget.show_docstring(doc)
General Comments 0
You need to be logged in to leave comments. Login now