##// END OF EJS Templates
Updated IPythonWidget to use new prompt information. Initial prompt requests are not implemented.
epatters -
Show More
@@ -1,1248 +1,1250 b''
1 1 # Standard library imports
2 2 import sys
3 3 from textwrap import dedent
4 4
5 5 # System library imports
6 6 from PyQt4 import QtCore, QtGui
7 7
8 8 # Local imports
9 9 from ansi_code_processor import QtAnsiCodeProcessor
10 10 from completion_widget import CompletionWidget
11 11
12 12
13 13 class ConsoleWidget(QtGui.QWidget):
14 14 """ An abstract base class for console-type widgets. This class has
15 15 functionality for:
16 16
17 17 * Maintaining a prompt and editing region
18 18 * Providing the traditional Unix-style console keyboard shortcuts
19 19 * Performing tab completion
20 20 * Paging text
21 21 * Handling ANSI escape codes
22 22
23 23 ConsoleWidget also provides a number of utility methods that will be
24 24 convenient to implementors of a console-style widget.
25 25 """
26 26
27 27 # Whether to process ANSI escape codes.
28 28 ansi_codes = True
29 29
30 30 # The maximum number of lines of text before truncation.
31 31 buffer_size = 500
32 32
33 33 # Whether to use a list widget or plain text output for tab completion.
34 34 gui_completion = True
35 35
36 36 # Whether to override ShortcutEvents for the keybindings defined by this
37 37 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
38 38 # priority (when it has focus) over, e.g., window-level menu shortcuts.
39 39 override_shortcuts = False
40 40
41 41 # Signals that indicate ConsoleWidget state.
42 42 copy_available = QtCore.pyqtSignal(bool)
43 43 redo_available = QtCore.pyqtSignal(bool)
44 44 undo_available = QtCore.pyqtSignal(bool)
45 45
46 46 # Signal emitted when paging is needed and the paging style has been
47 47 # specified as 'custom'.
48 48 custom_page_requested = QtCore.pyqtSignal(object)
49 49
50 50 # Protected class variables.
51 51 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
52 52 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
53 53 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
54 54 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
55 55 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
56 56 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
57 57 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
58 58 _shortcuts = set(_ctrl_down_remap.keys() +
59 59 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
60 60
61 61 #---------------------------------------------------------------------------
62 62 # 'QObject' interface
63 63 #---------------------------------------------------------------------------
64 64
65 65 def __init__(self, kind='plain', paging='inside', parent=None):
66 66 """ Create a ConsoleWidget.
67 67
68 68 Parameters
69 69 ----------
70 70 kind : str, optional [default 'plain']
71 71 The type of underlying text widget to use. Valid values are 'plain',
72 72 which specifies a QPlainTextEdit, and 'rich', which specifies a
73 73 QTextEdit.
74 74
75 75 paging : str, optional [default 'inside']
76 76 The type of paging to use. Valid values are:
77 77 'inside' : The widget pages like a traditional terminal pager.
78 78 'hsplit' : When paging is requested, the widget is split
79 79 horizontally. The top pane contains the console,
80 80 and the bottom pane contains the paged text.
81 81 'vsplit' : Similar to 'hsplit', except that a vertical splitter
82 82 used.
83 83 'custom' : No action is taken by the widget beyond emitting a
84 84 'custom_page_requested(str)' signal.
85 85 'none' : The text is written directly to the console.
86 86
87 87 parent : QWidget, optional [default None]
88 88 The parent for this widget.
89 89 """
90 90 super(ConsoleWidget, self).__init__(parent)
91 91
92 92 # Create the layout and underlying text widget.
93 93 layout = QtGui.QStackedLayout(self)
94 94 layout.setMargin(0)
95 95 self._control = self._create_control(kind)
96 96 self._page_control = None
97 97 self._splitter = None
98 98 if paging in ('hsplit', 'vsplit'):
99 99 self._splitter = QtGui.QSplitter()
100 100 if paging == 'hsplit':
101 101 self._splitter.setOrientation(QtCore.Qt.Horizontal)
102 102 else:
103 103 self._splitter.setOrientation(QtCore.Qt.Vertical)
104 104 self._splitter.addWidget(self._control)
105 105 layout.addWidget(self._splitter)
106 106 else:
107 107 layout.addWidget(self._control)
108 108
109 109 # Create the paging widget, if necessary.
110 110 self._page_style = paging
111 111 if paging in ('inside', 'hsplit', 'vsplit'):
112 112 self._page_control = self._create_page_control()
113 113 if self._splitter:
114 114 self._page_control.hide()
115 115 self._splitter.addWidget(self._page_control)
116 116 else:
117 117 layout.addWidget(self._page_control)
118 118 elif paging not in ('custom', 'none'):
119 119 raise ValueError('Paging style %s unknown.' % repr(paging))
120 120
121 121 # Initialize protected variables. Some variables contain useful state
122 122 # information for subclasses; they should be considered read-only.
123 123 self._ansi_processor = QtAnsiCodeProcessor()
124 124 self._completion_widget = CompletionWidget(self._control)
125 125 self._continuation_prompt = '> '
126 126 self._continuation_prompt_html = None
127 127 self._executing = False
128 128 self._prompt = ''
129 129 self._prompt_html = None
130 130 self._prompt_pos = 0
131 131 self._reading = False
132 132 self._reading_callback = None
133 133 self._tab_width = 8
134 134
135 135 # Set a monospaced font.
136 136 self.reset_font()
137 137
138 138 def eventFilter(self, obj, event):
139 139 """ Reimplemented to ensure a console-like behavior in the underlying
140 140 text widget.
141 141 """
142 142 # Re-map keys for all filtered widgets.
143 143 etype = event.type()
144 144 if etype == QtCore.QEvent.KeyPress and \
145 145 self._control_key_down(event.modifiers()) and \
146 146 event.key() in self._ctrl_down_remap:
147 147 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
148 148 self._ctrl_down_remap[event.key()],
149 149 QtCore.Qt.NoModifier)
150 150 QtGui.qApp.sendEvent(obj, new_event)
151 151 return True
152 152
153 153 # Override shortucts for all filtered widgets. Note that on Mac OS it is
154 154 # always unnecessary to override shortcuts, hence the check below (users
155 155 # should just use the Control key instead of the Command key).
156 156 elif etype == QtCore.QEvent.ShortcutOverride and \
157 157 sys.platform != 'darwin' and \
158 158 self._control_key_down(event.modifiers()) and \
159 159 event.key() in self._shortcuts:
160 160 event.accept()
161 161 return False
162 162
163 163 elif obj == self._control:
164 164 # Disable moving text by drag and drop.
165 165 if etype == QtCore.QEvent.DragMove:
166 166 return True
167 167
168 168 elif etype == QtCore.QEvent.KeyPress:
169 169 return self._event_filter_console_keypress(event)
170 170
171 171 elif obj == self._page_control:
172 172 if etype == QtCore.QEvent.KeyPress:
173 173 return self._event_filter_page_keypress(event)
174 174
175 175 return super(ConsoleWidget, self).eventFilter(obj, event)
176 176
177 177 #---------------------------------------------------------------------------
178 178 # 'QWidget' interface
179 179 #---------------------------------------------------------------------------
180 180
181 181 def sizeHint(self):
182 182 """ Reimplemented to suggest a size that is 80 characters wide and
183 183 25 lines high.
184 184 """
185 185 style = self.style()
186 186 opt = QtGui.QStyleOptionHeader()
187 187 font_metrics = QtGui.QFontMetrics(self.font)
188 188 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
189 189
190 190 width = font_metrics.width(' ') * 80
191 191 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
192 192 if self._page_style == 'hsplit':
193 193 width = width * 2 + splitwidth
194 194
195 195 height = font_metrics.height() * 25
196 196 if self._page_style == 'vsplit':
197 197 height = height * 2 + splitwidth
198 198
199 199 return QtCore.QSize(width, height)
200 200
201 201 #---------------------------------------------------------------------------
202 202 # 'ConsoleWidget' public interface
203 203 #---------------------------------------------------------------------------
204 204
205 205 def can_paste(self):
206 206 """ Returns whether text can be pasted from the clipboard.
207 207 """
208 208 # Accept only text that can be ASCII encoded.
209 209 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
210 210 text = QtGui.QApplication.clipboard().text()
211 211 if not text.isEmpty():
212 212 try:
213 213 str(text)
214 214 return True
215 215 except UnicodeEncodeError:
216 216 pass
217 217 return False
218 218
219 219 def clear(self, keep_input=False):
220 220 """ Clear the console, then write a new prompt. If 'keep_input' is set,
221 221 restores the old input buffer when the new prompt is written.
222 222 """
223 223 self._control.clear()
224 224 if keep_input:
225 225 input_buffer = self.input_buffer
226 226 self._show_prompt()
227 227 if keep_input:
228 228 self.input_buffer = input_buffer
229 229
230 230 def copy(self):
231 231 """ Copy the current selected text to the clipboard.
232 232 """
233 233 self._control.copy()
234 234
235 235 def execute(self, source=None, hidden=False, interactive=False):
236 236 """ Executes source or the input buffer, possibly prompting for more
237 237 input.
238 238
239 239 Parameters:
240 240 -----------
241 241 source : str, optional
242 242
243 243 The source to execute. If not specified, the input buffer will be
244 244 used. If specified and 'hidden' is False, the input buffer will be
245 245 replaced with the source before execution.
246 246
247 247 hidden : bool, optional (default False)
248 248
249 249 If set, no output will be shown and the prompt will not be modified.
250 250 In other words, it will be completely invisible to the user that
251 251 an execution has occurred.
252 252
253 253 interactive : bool, optional (default False)
254 254
255 255 Whether the console is to treat the source as having been manually
256 256 entered by the user. The effect of this parameter depends on the
257 257 subclass implementation.
258 258
259 259 Raises:
260 260 -------
261 261 RuntimeError
262 262 If incomplete input is given and 'hidden' is True. In this case,
263 263 it is not possible to prompt for more input.
264 264
265 265 Returns:
266 266 --------
267 267 A boolean indicating whether the source was executed.
268 268 """
269 269 if not hidden:
270 270 if source is not None:
271 271 self.input_buffer = source
272 272
273 273 self._append_plain_text('\n')
274 274 self._executing_input_buffer = self.input_buffer
275 275 self._executing = True
276 276 self._prompt_finished()
277 277
278 278 real_source = self.input_buffer if source is None else source
279 279 complete = self._is_complete(real_source, interactive)
280 280 if complete:
281 281 if not hidden:
282 282 # The maximum block count is only in effect during execution.
283 283 # This ensures that _prompt_pos does not become invalid due to
284 284 # text truncation.
285 285 self._control.document().setMaximumBlockCount(self.buffer_size)
286 286 self._execute(real_source, hidden)
287 287 elif hidden:
288 288 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
289 289 else:
290 290 self._show_continuation_prompt()
291 291
292 292 return complete
293 293
294 294 def _get_input_buffer(self):
295 295 """ The text that the user has entered entered at the current prompt.
296 296 """
297 297 # If we're executing, the input buffer may not even exist anymore due to
298 298 # the limit imposed by 'buffer_size'. Therefore, we store it.
299 299 if self._executing:
300 300 return self._executing_input_buffer
301 301
302 302 cursor = self._get_end_cursor()
303 303 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
304 304 input_buffer = str(cursor.selection().toPlainText())
305 305
306 306 # Strip out continuation prompts.
307 307 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
308 308
309 309 def _set_input_buffer(self, string):
310 310 """ Replaces the text in the input buffer with 'string'.
311 311 """
312 312 # For now, it is an error to modify the input buffer during execution.
313 313 if self._executing:
314 314 raise RuntimeError("Cannot change input buffer during execution.")
315 315
316 316 # Remove old text.
317 317 cursor = self._get_end_cursor()
318 318 cursor.beginEditBlock()
319 319 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
320 320 cursor.removeSelectedText()
321 321
322 322 # Insert new text with continuation prompts.
323 323 lines = string.splitlines(True)
324 324 if lines:
325 325 self._append_plain_text(lines[0])
326 326 for i in xrange(1, len(lines)):
327 327 if self._continuation_prompt_html is None:
328 328 self._append_plain_text(self._continuation_prompt)
329 329 else:
330 330 self._append_html(self._continuation_prompt_html)
331 331 self._append_plain_text(lines[i])
332 332 cursor.endEditBlock()
333 333 self._control.moveCursor(QtGui.QTextCursor.End)
334 334
335 335 input_buffer = property(_get_input_buffer, _set_input_buffer)
336 336
337 337 def _get_font(self):
338 338 """ The base font being used by the ConsoleWidget.
339 339 """
340 340 return self._control.document().defaultFont()
341 341
342 342 def _set_font(self, font):
343 343 """ Sets the base font for the ConsoleWidget to the specified QFont.
344 344 """
345 345 font_metrics = QtGui.QFontMetrics(font)
346 346 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
347 347
348 348 self._completion_widget.setFont(font)
349 349 self._control.document().setDefaultFont(font)
350 350 if self._page_control:
351 351 self._page_control.document().setDefaultFont(font)
352 352
353 353 font = property(_get_font, _set_font)
354 354
355 355 def paste(self):
356 356 """ Paste the contents of the clipboard into the input region.
357 357 """
358 358 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
359 359 try:
360 360 text = str(QtGui.QApplication.clipboard().text())
361 361 except UnicodeEncodeError:
362 362 pass
363 363 else:
364 364 self._insert_plain_text_into_buffer(dedent(text))
365 365
366 366 def print_(self, printer):
367 367 """ Print the contents of the ConsoleWidget to the specified QPrinter.
368 368 """
369 369 self._control.print_(printer)
370 370
371 371 def redo(self):
372 372 """ Redo the last operation. If there is no operation to redo, nothing
373 373 happens.
374 374 """
375 375 self._control.redo()
376 376
377 377 def reset_font(self):
378 378 """ Sets the font to the default fixed-width font for this platform.
379 379 """
380 380 if sys.platform == 'win32':
381 381 name = 'Courier'
382 382 elif sys.platform == 'darwin':
383 383 name = 'Monaco'
384 384 else:
385 385 name = 'Monospace'
386 386 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
387 387 font.setStyleHint(QtGui.QFont.TypeWriter)
388 388 self._set_font(font)
389 389
390 390 def select_all(self):
391 391 """ Selects all the text in the buffer.
392 392 """
393 393 self._control.selectAll()
394 394
395 395 def _get_tab_width(self):
396 396 """ The width (in terms of space characters) for tab characters.
397 397 """
398 398 return self._tab_width
399 399
400 400 def _set_tab_width(self, tab_width):
401 401 """ Sets the width (in terms of space characters) for tab characters.
402 402 """
403 403 font_metrics = QtGui.QFontMetrics(self.font)
404 404 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
405 405
406 406 self._tab_width = tab_width
407 407
408 408 tab_width = property(_get_tab_width, _set_tab_width)
409 409
410 410 def undo(self):
411 411 """ Undo the last operation. If there is no operation to undo, nothing
412 412 happens.
413 413 """
414 414 self._control.undo()
415 415
416 416 #---------------------------------------------------------------------------
417 417 # 'ConsoleWidget' abstract interface
418 418 #---------------------------------------------------------------------------
419 419
420 420 def _is_complete(self, source, interactive):
421 421 """ Returns whether 'source' can be executed. When triggered by an
422 422 Enter/Return key press, 'interactive' is True; otherwise, it is
423 423 False.
424 424 """
425 425 raise NotImplementedError
426 426
427 427 def _execute(self, source, hidden):
428 428 """ Execute 'source'. If 'hidden', do not show any output.
429 429 """
430 430 raise NotImplementedError
431 431
432 432 def _execute_interrupt(self):
433 433 """ Attempts to stop execution. Returns whether this method has an
434 434 implementation.
435 435 """
436 436 return False
437 437
438 438 def _prompt_started_hook(self):
439 439 """ Called immediately after a new prompt is displayed.
440 440 """
441 441 pass
442 442
443 443 def _prompt_finished_hook(self):
444 444 """ Called immediately after a prompt is finished, i.e. when some input
445 445 will be processed and a new prompt displayed.
446 446 """
447 447 pass
448 448
449 449 def _up_pressed(self):
450 450 """ Called when the up key is pressed. Returns whether to continue
451 451 processing the event.
452 452 """
453 453 return True
454 454
455 455 def _down_pressed(self):
456 456 """ Called when the down key is pressed. Returns whether to continue
457 457 processing the event.
458 458 """
459 459 return True
460 460
461 461 def _tab_pressed(self):
462 462 """ Called when the tab key is pressed. Returns whether to continue
463 463 processing the event.
464 464 """
465 465 return False
466 466
467 467 #--------------------------------------------------------------------------
468 468 # 'ConsoleWidget' protected interface
469 469 #--------------------------------------------------------------------------
470 470
471 471 def _append_html(self, html):
472 472 """ Appends html at the end of the console buffer.
473 473 """
474 474 cursor = self._get_end_cursor()
475 475 self._insert_html(cursor, html)
476 476
477 477 def _append_html_fetching_plain_text(self, html):
478 478 """ Appends 'html', then returns the plain text version of it.
479 479 """
480 480 cursor = self._get_end_cursor()
481 481 return self._insert_html_fetching_plain_text(cursor, html)
482 482
483 483 def _append_plain_text(self, text):
484 484 """ Appends plain text at the end of the console buffer, processing
485 485 ANSI codes if enabled.
486 486 """
487 487 cursor = self._get_end_cursor()
488 488 self._insert_plain_text(cursor, text)
489 489
490 490 def _append_plain_text_keeping_prompt(self, text):
491 491 """ Writes 'text' after the current prompt, then restores the old prompt
492 492 with its old input buffer.
493 493 """
494 494 input_buffer = self.input_buffer
495 495 self._append_plain_text('\n')
496 496 self._prompt_finished()
497 497
498 498 self._append_plain_text(text)
499 499 self._show_prompt()
500 500 self.input_buffer = input_buffer
501 501
502 502 def _complete_with_items(self, cursor, items):
503 503 """ Performs completion with 'items' at the specified cursor location.
504 504 """
505 505 if len(items) == 1:
506 506 cursor.setPosition(self._control.textCursor().position(),
507 507 QtGui.QTextCursor.KeepAnchor)
508 508 cursor.insertText(items[0])
509 509 elif len(items) > 1:
510 510 if self.gui_completion:
511 511 self._completion_widget.show_items(cursor, items)
512 512 else:
513 513 text = self._format_as_columns(items)
514 514 self._append_plain_text_keeping_prompt(text)
515 515
516 516 def _control_key_down(self, modifiers):
517 517 """ Given a KeyboardModifiers flags object, return whether the Control
518 518 key is down (on Mac OS, treat the Command key as a synonym for
519 519 Control).
520 520 """
521 521 down = bool(modifiers & QtCore.Qt.ControlModifier)
522 522
523 523 # Note: on Mac OS, ControlModifier corresponds to the Command key while
524 524 # MetaModifier corresponds to the Control key.
525 525 if sys.platform == 'darwin':
526 526 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
527 527
528 528 return down
529 529
530 530 def _create_control(self, kind):
531 531 """ Creates and connects the underlying text widget.
532 532 """
533 533 if kind == 'plain':
534 534 control = QtGui.QPlainTextEdit()
535 535 elif kind == 'rich':
536 536 control = QtGui.QTextEdit()
537 537 control.setAcceptRichText(False)
538 538 else:
539 539 raise ValueError("Kind %s unknown." % repr(kind))
540 540 control.installEventFilter(self)
541 541 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
542 542 control.customContextMenuRequested.connect(self._show_context_menu)
543 543 control.copyAvailable.connect(self.copy_available)
544 544 control.redoAvailable.connect(self.redo_available)
545 545 control.undoAvailable.connect(self.undo_available)
546 546 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
547 547 return control
548 548
549 549 def _create_page_control(self):
550 550 """ Creates and connects the underlying paging widget.
551 551 """
552 552 control = QtGui.QPlainTextEdit()
553 553 control.installEventFilter(self)
554 554 control.setReadOnly(True)
555 555 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
556 556 return control
557 557
558 558 def _event_filter_console_keypress(self, event):
559 559 """ Filter key events for the underlying text widget to create a
560 560 console-like interface.
561 561 """
562 562 intercepted = False
563 563 cursor = self._control.textCursor()
564 564 position = cursor.position()
565 565 key = event.key()
566 566 ctrl_down = self._control_key_down(event.modifiers())
567 567 alt_down = event.modifiers() & QtCore.Qt.AltModifier
568 568 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
569 569
570 570 if event.matches(QtGui.QKeySequence.Paste):
571 571 # Call our paste instead of the underlying text widget's.
572 572 self.paste()
573 573 intercepted = True
574 574
575 575 elif ctrl_down:
576 576 if key == QtCore.Qt.Key_C:
577 577 intercepted = self._executing and self._execute_interrupt()
578 578
579 579 elif key == QtCore.Qt.Key_K:
580 580 if self._in_buffer(position):
581 581 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
582 582 QtGui.QTextCursor.KeepAnchor)
583 583 cursor.removeSelectedText()
584 584 intercepted = True
585 585
586 586 elif key == QtCore.Qt.Key_X:
587 587 intercepted = True
588 588
589 589 elif key == QtCore.Qt.Key_Y:
590 590 self.paste()
591 591 intercepted = True
592 592
593 593 elif alt_down:
594 594 if key == QtCore.Qt.Key_B:
595 595 self._set_cursor(self._get_word_start_cursor(position))
596 596 intercepted = True
597 597
598 598 elif key == QtCore.Qt.Key_F:
599 599 self._set_cursor(self._get_word_end_cursor(position))
600 600 intercepted = True
601 601
602 602 elif key == QtCore.Qt.Key_Backspace:
603 603 cursor = self._get_word_start_cursor(position)
604 604 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
605 605 cursor.removeSelectedText()
606 606 intercepted = True
607 607
608 608 elif key == QtCore.Qt.Key_D:
609 609 cursor = self._get_word_end_cursor(position)
610 610 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
611 611 cursor.removeSelectedText()
612 612 intercepted = True
613 613
614 614 else:
615 615 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
616 616 if self._reading:
617 617 self._append_plain_text('\n')
618 618 self._reading = False
619 619 if self._reading_callback:
620 620 self._reading_callback()
621 621 elif not self._executing:
622 622 self.execute(interactive=True)
623 623 intercepted = True
624 624
625 625 elif key == QtCore.Qt.Key_Up:
626 626 if self._reading or not self._up_pressed():
627 627 intercepted = True
628 628 else:
629 629 prompt_line = self._get_prompt_cursor().blockNumber()
630 630 intercepted = cursor.blockNumber() <= prompt_line
631 631
632 632 elif key == QtCore.Qt.Key_Down:
633 633 if self._reading or not self._down_pressed():
634 634 intercepted = True
635 635 else:
636 636 end_line = self._get_end_cursor().blockNumber()
637 637 intercepted = cursor.blockNumber() == end_line
638 638
639 639 elif key == QtCore.Qt.Key_Tab:
640 640 if self._reading:
641 641 intercepted = False
642 642 else:
643 643 intercepted = not self._tab_pressed()
644 644
645 645 elif key == QtCore.Qt.Key_Left:
646 646 intercepted = not self._in_buffer(position - 1)
647 647
648 648 elif key == QtCore.Qt.Key_Home:
649 649 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
650 650 start_line = cursor.blockNumber()
651 651 if start_line == self._get_prompt_cursor().blockNumber():
652 652 start_pos = self._prompt_pos
653 653 else:
654 654 start_pos = cursor.position()
655 655 start_pos += len(self._continuation_prompt)
656 656 if shift_down and self._in_buffer(position):
657 657 self._set_selection(position, start_pos)
658 658 else:
659 659 self._set_position(start_pos)
660 660 intercepted = True
661 661
662 662 elif key == QtCore.Qt.Key_Backspace:
663 663
664 664 # Line deletion (remove continuation prompt)
665 665 len_prompt = len(self._continuation_prompt)
666 666 if not self._reading and \
667 667 cursor.columnNumber() == len_prompt and \
668 668 position != self._prompt_pos:
669 669 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
670 670 QtGui.QTextCursor.KeepAnchor)
671 671 cursor.removeSelectedText()
672 672 cursor.deletePreviousChar()
673 673 intercepted = True
674 674
675 675 # Regular backwards deletion
676 676 else:
677 677 anchor = cursor.anchor()
678 678 if anchor == position:
679 679 intercepted = not self._in_buffer(position - 1)
680 680 else:
681 681 intercepted = not self._in_buffer(min(anchor, position))
682 682
683 683 elif key == QtCore.Qt.Key_Delete:
684 684 anchor = cursor.anchor()
685 685 intercepted = not self._in_buffer(min(anchor, position))
686 686
687 687 # Don't move the cursor if control is down to allow copy-paste using
688 688 # the keyboard in any part of the buffer.
689 689 if not ctrl_down:
690 690 self._keep_cursor_in_buffer()
691 691
692 692 return intercepted
693 693
694 694 def _event_filter_page_keypress(self, event):
695 695 """ Filter key events for the paging widget to create console-like
696 696 interface.
697 697 """
698 698 key = event.key()
699 699
700 700 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
701 701 if self._splitter:
702 702 self._page_control.hide()
703 703 else:
704 704 self.layout().setCurrentWidget(self._control)
705 705 return True
706 706
707 707 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
708 708 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
709 709 QtCore.Qt.Key_Down,
710 710 QtCore.Qt.NoModifier)
711 711 QtGui.qApp.sendEvent(self._page_control, new_event)
712 712 return True
713 713
714 714 return False
715 715
716 716 def _format_as_columns(self, items, separator=' '):
717 717 """ Transform a list of strings into a single string with columns.
718 718
719 719 Parameters
720 720 ----------
721 721 items : sequence of strings
722 722 The strings to process.
723 723
724 724 separator : str, optional [default is two spaces]
725 725 The string that separates columns.
726 726
727 727 Returns
728 728 -------
729 729 The formatted string.
730 730 """
731 731 # Note: this code is adapted from columnize 0.3.2.
732 732 # See http://code.google.com/p/pycolumnize/
733 733
734 734 width = self._control.viewport().width()
735 735 char_width = QtGui.QFontMetrics(self.font).width(' ')
736 736 displaywidth = max(5, width / char_width)
737 737
738 738 # Some degenerate cases.
739 739 size = len(items)
740 740 if size == 0:
741 741 return '\n'
742 742 elif size == 1:
743 743 return '%s\n' % str(items[0])
744 744
745 745 # Try every row count from 1 upwards
746 746 array_index = lambda nrows, row, col: nrows*col + row
747 747 for nrows in range(1, size):
748 748 ncols = (size + nrows - 1) // nrows
749 749 colwidths = []
750 750 totwidth = -len(separator)
751 751 for col in range(ncols):
752 752 # Get max column width for this column
753 753 colwidth = 0
754 754 for row in range(nrows):
755 755 i = array_index(nrows, row, col)
756 756 if i >= size: break
757 757 x = items[i]
758 758 colwidth = max(colwidth, len(x))
759 759 colwidths.append(colwidth)
760 760 totwidth += colwidth + len(separator)
761 761 if totwidth > displaywidth:
762 762 break
763 763 if totwidth <= displaywidth:
764 764 break
765 765
766 766 # The smallest number of rows computed and the max widths for each
767 767 # column has been obtained. Now we just have to format each of the rows.
768 768 string = ''
769 769 for row in range(nrows):
770 770 texts = []
771 771 for col in range(ncols):
772 772 i = row + nrows*col
773 773 if i >= size:
774 774 texts.append('')
775 775 else:
776 776 texts.append(items[i])
777 777 while texts and not texts[-1]:
778 778 del texts[-1]
779 779 for col in range(len(texts)):
780 780 texts[col] = texts[col].ljust(colwidths[col])
781 781 string += '%s\n' % str(separator.join(texts))
782 782 return string
783 783
784 784 def _get_block_plain_text(self, block):
785 785 """ Given a QTextBlock, return its unformatted text.
786 786 """
787 787 cursor = QtGui.QTextCursor(block)
788 788 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
789 789 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
790 790 QtGui.QTextCursor.KeepAnchor)
791 791 return str(cursor.selection().toPlainText())
792 792
793 793 def _get_cursor(self):
794 794 """ Convenience method that returns a cursor for the current position.
795 795 """
796 796 return self._control.textCursor()
797 797
798 798 def _get_end_cursor(self):
799 799 """ Convenience method that returns a cursor for the last character.
800 800 """
801 801 cursor = self._control.textCursor()
802 802 cursor.movePosition(QtGui.QTextCursor.End)
803 803 return cursor
804 804
805 805 def _get_input_buffer_cursor_line(self):
806 806 """ The text in the line of the input buffer in which the user's cursor
807 807 rests. Returns a string if there is such a line; otherwise, None.
808 808 """
809 809 if self._executing:
810 810 return None
811 811 cursor = self._control.textCursor()
812 812 if cursor.position() >= self._prompt_pos:
813 813 text = self._get_block_plain_text(cursor.block())
814 814 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
815 815 return text[len(self._prompt):]
816 816 else:
817 817 return text[len(self._continuation_prompt):]
818 818 else:
819 819 return None
820 820
821 821 def _get_prompt_cursor(self):
822 822 """ Convenience method that returns a cursor for the prompt position.
823 823 """
824 824 cursor = self._control.textCursor()
825 825 cursor.setPosition(self._prompt_pos)
826 826 return cursor
827 827
828 828 def _get_selection_cursor(self, start, end):
829 829 """ Convenience method that returns a cursor with text selected between
830 830 the positions 'start' and 'end'.
831 831 """
832 832 cursor = self._control.textCursor()
833 833 cursor.setPosition(start)
834 834 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
835 835 return cursor
836 836
837 837 def _get_word_start_cursor(self, position):
838 838 """ Find the start of the word to the left the given position. If a
839 839 sequence of non-word characters precedes the first word, skip over
840 840 them. (This emulates the behavior of bash, emacs, etc.)
841 841 """
842 842 document = self._control.document()
843 843 position -= 1
844 844 while position >= self._prompt_pos and \
845 845 not document.characterAt(position).isLetterOrNumber():
846 846 position -= 1
847 847 while position >= self._prompt_pos and \
848 848 document.characterAt(position).isLetterOrNumber():
849 849 position -= 1
850 850 cursor = self._control.textCursor()
851 851 cursor.setPosition(position + 1)
852 852 return cursor
853 853
854 854 def _get_word_end_cursor(self, position):
855 855 """ Find the end of the word to the right the given position. If a
856 856 sequence of non-word characters precedes the first word, skip over
857 857 them. (This emulates the behavior of bash, emacs, etc.)
858 858 """
859 859 document = self._control.document()
860 860 end = self._get_end_cursor().position()
861 861 while position < end and \
862 862 not document.characterAt(position).isLetterOrNumber():
863 863 position += 1
864 864 while position < end and \
865 865 document.characterAt(position).isLetterOrNumber():
866 866 position += 1
867 867 cursor = self._control.textCursor()
868 868 cursor.setPosition(position)
869 869 return cursor
870 870
871 871 def _insert_html(self, cursor, html):
872 872 """ Inserts HTML using the specified cursor in such a way that future
873 873 formatting is unaffected.
874 874 """
875 875 cursor.beginEditBlock()
876 876 cursor.insertHtml(html)
877 877
878 878 # After inserting HTML, the text document "remembers" it's in "html
879 879 # mode", which means that subsequent calls adding plain text will result
880 880 # in unwanted formatting, lost tab characters, etc. The following code
881 # hacks around this behavior, which I consider to be a bug in Qt.
882 cursor.movePosition(QtGui.QTextCursor.Left,
881 # hacks around this behavior, which I consider to be a bug in Qt, by
882 # (crudely) resetting the document's style state.
883 cursor.movePosition(QtGui.QTextCursor.Left,
883 884 QtGui.QTextCursor.KeepAnchor)
884 885 if cursor.selection().toPlainText() == ' ':
885 886 cursor.removeSelectedText()
886 cursor.movePosition(QtGui.QTextCursor.Right)
887 else:
888 cursor.movePosition(QtGui.QTextCursor.Right)
887 889 cursor.insertText(' ', QtGui.QTextCharFormat())
888 890 cursor.endEditBlock()
889 891
890 892 def _insert_html_fetching_plain_text(self, cursor, html):
891 893 """ Inserts HTML using the specified cursor, then returns its plain text
892 894 version.
893 895 """
894 896 start = cursor.position()
895 897 self._insert_html(cursor, html)
896 898 end = cursor.position()
897 899 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
898 900 text = str(cursor.selection().toPlainText())
899 901 cursor.setPosition(end)
900 902 return text
901 903
902 904 def _insert_plain_text(self, cursor, text):
903 905 """ Inserts plain text using the specified cursor, processing ANSI codes
904 906 if enabled.
905 907 """
906 908 cursor.beginEditBlock()
907 909 if self.ansi_codes:
908 910 for substring in self._ansi_processor.split_string(text):
909 911 for action in self._ansi_processor.actions:
910 912 if action.kind == 'erase' and action.area == 'screen':
911 913 cursor.select(QtGui.QTextCursor.Document)
912 914 cursor.removeSelectedText()
913 915 format = self._ansi_processor.get_format()
914 916 cursor.insertText(substring, format)
915 917 else:
916 918 cursor.insertText(text)
917 919 cursor.endEditBlock()
918 920
919 921 def _insert_plain_text_into_buffer(self, text):
920 922 """ Inserts text into the input buffer at the current cursor position,
921 923 ensuring that continuation prompts are inserted as necessary.
922 924 """
923 925 lines = str(text).splitlines(True)
924 926 if lines:
925 927 self._keep_cursor_in_buffer()
926 928 cursor = self._control.textCursor()
927 929 cursor.beginEditBlock()
928 930 cursor.insertText(lines[0])
929 931 for line in lines[1:]:
930 932 if self._continuation_prompt_html is None:
931 933 cursor.insertText(self._continuation_prompt)
932 934 else:
933 935 self._continuation_prompt = \
934 936 self._insert_html_fetching_plain_text(
935 937 cursor, self._continuation_prompt_html)
936 938 cursor.insertText(line)
937 939 cursor.endEditBlock()
938 940 self._control.setTextCursor(cursor)
939 941
940 942 def _in_buffer(self, position):
941 943 """ Returns whether the given position is inside the editing region.
942 944 """
943 945 cursor = self._control.textCursor()
944 946 cursor.setPosition(position)
945 947 line = cursor.blockNumber()
946 948 prompt_line = self._get_prompt_cursor().blockNumber()
947 949 if line == prompt_line:
948 950 return position >= self._prompt_pos
949 951 elif line > prompt_line:
950 952 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
951 953 prompt_pos = cursor.position() + len(self._continuation_prompt)
952 954 return position >= prompt_pos
953 955 return False
954 956
955 957 def _keep_cursor_in_buffer(self):
956 958 """ Ensures that the cursor is inside the editing region. Returns
957 959 whether the cursor was moved.
958 960 """
959 961 cursor = self._control.textCursor()
960 962 if self._in_buffer(cursor.position()):
961 963 return False
962 964 else:
963 965 cursor.movePosition(QtGui.QTextCursor.End)
964 966 self._control.setTextCursor(cursor)
965 967 return True
966 968
967 969 def _page(self, text):
968 970 """ Displays text using the pager.
969 971 """
970 972 if self._page_style == 'custom':
971 973 self.custom_page_requested.emit(text)
972 974 elif self._page_style == 'none':
973 975 self._append_plain_text(text)
974 976 else:
975 977 self._page_control.clear()
976 978 cursor = self._page_control.textCursor()
977 979 self._insert_plain_text(cursor, text)
978 980 self._page_control.moveCursor(QtGui.QTextCursor.Start)
979 981
980 982 self._page_control.viewport().resize(self._control.size())
981 983 if self._splitter:
982 984 self._page_control.show()
983 985 self._page_control.setFocus()
984 986 else:
985 987 self.layout().setCurrentWidget(self._page_control)
986 988
987 989 def _prompt_started(self):
988 990 """ Called immediately after a new prompt is displayed.
989 991 """
990 992 # Temporarily disable the maximum block count to permit undo/redo and
991 993 # to ensure that the prompt position does not change due to truncation.
992 994 self._control.document().setMaximumBlockCount(0)
993 995 self._control.setUndoRedoEnabled(True)
994 996
995 997 self._control.setReadOnly(False)
996 998 self._control.moveCursor(QtGui.QTextCursor.End)
997 999
998 1000 self._executing = False
999 1001 self._prompt_started_hook()
1000 1002
1001 1003 def _prompt_finished(self):
1002 1004 """ Called immediately after a prompt is finished, i.e. when some input
1003 1005 will be processed and a new prompt displayed.
1004 1006 """
1005 1007 self._control.setUndoRedoEnabled(False)
1006 1008 self._control.setReadOnly(True)
1007 1009 self._prompt_finished_hook()
1008 1010
1009 1011 def _readline(self, prompt='', callback=None):
1010 1012 """ Reads one line of input from the user.
1011 1013
1012 1014 Parameters
1013 1015 ----------
1014 1016 prompt : str, optional
1015 1017 The prompt to print before reading the line.
1016 1018
1017 1019 callback : callable, optional
1018 1020 A callback to execute with the read line. If not specified, input is
1019 1021 read *synchronously* and this method does not return until it has
1020 1022 been read.
1021 1023
1022 1024 Returns
1023 1025 -------
1024 1026 If a callback is specified, returns nothing. Otherwise, returns the
1025 1027 input string with the trailing newline stripped.
1026 1028 """
1027 1029 if self._reading:
1028 1030 raise RuntimeError('Cannot read a line. Widget is already reading.')
1029 1031
1030 1032 if not callback and not self.isVisible():
1031 1033 # If the user cannot see the widget, this function cannot return.
1032 1034 raise RuntimeError('Cannot synchronously read a line if the widget'
1033 1035 'is not visible!')
1034 1036
1035 1037 self._reading = True
1036 1038 self._show_prompt(prompt, newline=False)
1037 1039
1038 1040 if callback is None:
1039 1041 self._reading_callback = None
1040 1042 while self._reading:
1041 1043 QtCore.QCoreApplication.processEvents()
1042 1044 return self.input_buffer.rstrip('\n')
1043 1045
1044 1046 else:
1045 1047 self._reading_callback = lambda: \
1046 1048 callback(self.input_buffer.rstrip('\n'))
1047 1049
1048 1050 def _reset(self):
1049 1051 """ Clears the console and resets internal state variables.
1050 1052 """
1051 1053 self._control.clear()
1052 1054 self._executing = self._reading = False
1053 1055
1054 1056 def _set_continuation_prompt(self, prompt, html=False):
1055 1057 """ Sets the continuation prompt.
1056 1058
1057 1059 Parameters
1058 1060 ----------
1059 1061 prompt : str
1060 1062 The prompt to show when more input is needed.
1061 1063
1062 1064 html : bool, optional (default False)
1063 1065 If set, the prompt will be inserted as formatted HTML. Otherwise,
1064 1066 the prompt will be treated as plain text, though ANSI color codes
1065 1067 will be handled.
1066 1068 """
1067 1069 if html:
1068 1070 self._continuation_prompt_html = prompt
1069 1071 else:
1070 1072 self._continuation_prompt = prompt
1071 1073 self._continuation_prompt_html = None
1072 1074
1073 1075 def _set_cursor(self, cursor):
1074 1076 """ Convenience method to set the current cursor.
1075 1077 """
1076 1078 self._control.setTextCursor(cursor)
1077 1079
1078 1080 def _set_position(self, position):
1079 1081 """ Convenience method to set the position of the cursor.
1080 1082 """
1081 1083 cursor = self._control.textCursor()
1082 1084 cursor.setPosition(position)
1083 1085 self._control.setTextCursor(cursor)
1084 1086
1085 1087 def _set_selection(self, start, end):
1086 1088 """ Convenience method to set the current selected text.
1087 1089 """
1088 1090 self._control.setTextCursor(self._get_selection_cursor(start, end))
1089 1091
1090 1092 def _show_context_menu(self, pos):
1091 1093 """ Shows a context menu at the given QPoint (in widget coordinates).
1092 1094 """
1093 1095 menu = QtGui.QMenu()
1094 1096
1095 1097 copy_action = menu.addAction('Copy', self.copy)
1096 1098 copy_action.setEnabled(self._get_cursor().hasSelection())
1097 1099 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1098 1100
1099 1101 paste_action = menu.addAction('Paste', self.paste)
1100 1102 paste_action.setEnabled(self.can_paste())
1101 1103 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1102 1104
1103 1105 menu.addSeparator()
1104 1106 menu.addAction('Select All', self.select_all)
1105 1107
1106 1108 menu.exec_(self._control.mapToGlobal(pos))
1107 1109
1108 1110 def _show_prompt(self, prompt=None, html=False, newline=True):
1109 1111 """ Writes a new prompt at the end of the buffer.
1110 1112
1111 1113 Parameters
1112 1114 ----------
1113 1115 prompt : str, optional
1114 1116 The prompt to show. If not specified, the previous prompt is used.
1115 1117
1116 1118 html : bool, optional (default False)
1117 1119 Only relevant when a prompt is specified. If set, the prompt will
1118 1120 be inserted as formatted HTML. Otherwise, the prompt will be treated
1119 1121 as plain text, though ANSI color codes will be handled.
1120 1122
1121 1123 newline : bool, optional (default True)
1122 1124 If set, a new line will be written before showing the prompt if
1123 1125 there is not already a newline at the end of the buffer.
1124 1126 """
1125 1127 # Insert a preliminary newline, if necessary.
1126 1128 if newline:
1127 1129 cursor = self._get_end_cursor()
1128 1130 if cursor.position() > 0:
1129 1131 cursor.movePosition(QtGui.QTextCursor.Left,
1130 1132 QtGui.QTextCursor.KeepAnchor)
1131 1133 if str(cursor.selection().toPlainText()) != '\n':
1132 1134 self._append_plain_text('\n')
1133 1135
1134 1136 # Write the prompt.
1135 1137 if prompt is None:
1136 1138 if self._prompt_html is None:
1137 1139 self._append_plain_text(self._prompt)
1138 1140 else:
1139 1141 self._append_html(self._prompt_html)
1140 1142 else:
1141 1143 if html:
1142 1144 self._prompt = self._append_html_fetching_plain_text(prompt)
1143 1145 self._prompt_html = prompt
1144 1146 else:
1145 1147 self._append_plain_text(prompt)
1146 1148 self._prompt = prompt
1147 1149 self._prompt_html = None
1148 1150
1149 1151 self._prompt_pos = self._get_end_cursor().position()
1150 1152 self._prompt_started()
1151 1153
1152 1154 def _show_continuation_prompt(self):
1153 1155 """ Writes a new continuation prompt at the end of the buffer.
1154 1156 """
1155 1157 if self._continuation_prompt_html is None:
1156 1158 self._append_plain_text(self._continuation_prompt)
1157 1159 else:
1158 1160 self._continuation_prompt = self._append_html_fetching_plain_text(
1159 1161 self._continuation_prompt_html)
1160 1162
1161 1163 self._prompt_started()
1162 1164
1163 1165
1164 1166 class HistoryConsoleWidget(ConsoleWidget):
1165 1167 """ A ConsoleWidget that keeps a history of the commands that have been
1166 1168 executed.
1167 1169 """
1168 1170
1169 1171 #---------------------------------------------------------------------------
1170 1172 # 'object' interface
1171 1173 #---------------------------------------------------------------------------
1172 1174
1173 1175 def __init__(self, *args, **kw):
1174 1176 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1175 1177 self._history = []
1176 1178 self._history_index = 0
1177 1179
1178 1180 #---------------------------------------------------------------------------
1179 1181 # 'ConsoleWidget' public interface
1180 1182 #---------------------------------------------------------------------------
1181 1183
1182 1184 def execute(self, source=None, hidden=False, interactive=False):
1183 1185 """ Reimplemented to the store history.
1184 1186 """
1185 1187 if not hidden:
1186 1188 history = self.input_buffer if source is None else source
1187 1189
1188 1190 executed = super(HistoryConsoleWidget, self).execute(
1189 1191 source, hidden, interactive)
1190 1192
1191 1193 if executed and not hidden:
1192 1194 self._history.append(history.rstrip())
1193 1195 self._history_index = len(self._history)
1194 1196
1195 1197 return executed
1196 1198
1197 1199 #---------------------------------------------------------------------------
1198 1200 # 'ConsoleWidget' abstract interface
1199 1201 #---------------------------------------------------------------------------
1200 1202
1201 1203 def _up_pressed(self):
1202 1204 """ Called when the up key is pressed. Returns whether to continue
1203 1205 processing the event.
1204 1206 """
1205 1207 prompt_cursor = self._get_prompt_cursor()
1206 1208 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1207 1209 self.history_previous()
1208 1210
1209 1211 # Go to the first line of prompt for seemless history scrolling.
1210 1212 cursor = self._get_prompt_cursor()
1211 1213 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1212 1214 self._set_cursor(cursor)
1213 1215
1214 1216 return False
1215 1217 return True
1216 1218
1217 1219 def _down_pressed(self):
1218 1220 """ Called when the down key is pressed. Returns whether to continue
1219 1221 processing the event.
1220 1222 """
1221 1223 end_cursor = self._get_end_cursor()
1222 1224 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1223 1225 self.history_next()
1224 1226 return False
1225 1227 return True
1226 1228
1227 1229 #---------------------------------------------------------------------------
1228 1230 # 'HistoryConsoleWidget' interface
1229 1231 #---------------------------------------------------------------------------
1230 1232
1231 1233 def history_previous(self):
1232 1234 """ If possible, set the input buffer to the previous item in the
1233 1235 history.
1234 1236 """
1235 1237 if self._history_index > 0:
1236 1238 self._history_index -= 1
1237 1239 self.input_buffer = self._history[self._history_index]
1238 1240
1239 1241 def history_next(self):
1240 1242 """ Set the input buffer to the next item in the history, or a blank
1241 1243 line if there is no subsequent item.
1242 1244 """
1243 1245 if self._history_index < len(self._history):
1244 1246 self._history_index += 1
1245 1247 if self._history_index < len(self._history):
1246 1248 self.input_buffer = self._history[self._history_index]
1247 1249 else:
1248 1250 self.input_buffer = ''
@@ -1,349 +1,354 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 IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 13 from call_tip_widget import CallTipWidget
14 14 from completion_lexer import CompletionLexer
15 15 from console_widget import HistoryConsoleWidget
16 16 from pygments_highlighter import PygmentsHighlighter
17 17
18 18
19 19 class FrontendHighlighter(PygmentsHighlighter):
20 20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 21 prompts.
22 22 """
23 23
24 24 def __init__(self, frontend):
25 25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 26 self._current_offset = 0
27 27 self._frontend = frontend
28 28 self.highlighting_on = False
29 29
30 30 def highlightBlock(self, qstring):
31 31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 32 """
33 33 if not self.highlighting_on:
34 34 return
35 35
36 36 # The input to this function is unicode string that may contain
37 37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 38 # the string as plain text so we can compare it.
39 39 current_block = self.currentBlock()
40 40 string = self._frontend._get_block_plain_text(current_block)
41 41
42 42 # Decide whether to check for the regular or continuation prompt.
43 43 if current_block.contains(self._frontend._prompt_pos):
44 44 prompt = self._frontend._prompt
45 45 else:
46 46 prompt = self._frontend._continuation_prompt
47 47
48 48 # Don't highlight the part of the string that contains the prompt.
49 49 if string.startswith(prompt):
50 50 self._current_offset = len(prompt)
51 51 qstring.remove(0, len(prompt))
52 52 else:
53 53 self._current_offset = 0
54 54
55 55 PygmentsHighlighter.highlightBlock(self, qstring)
56 56
57 57 def setFormat(self, start, count, format):
58 58 """ Reimplemented to highlight selectively.
59 59 """
60 60 start += self._current_offset
61 61 PygmentsHighlighter.setFormat(self, start, count, format)
62 62
63 63
64 64 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
65 65 """ A Qt frontend for a generic Python kernel.
66 66 """
67 67
68 68 # Emitted when an 'execute_reply' has been received from the kernel and
69 69 # processed by the FrontendWidget.
70 70 executed = QtCore.pyqtSignal(object)
71 71
72 72 #---------------------------------------------------------------------------
73 73 # 'object' interface
74 74 #---------------------------------------------------------------------------
75 75
76 76 def __init__(self, *args, **kw):
77 77 super(FrontendWidget, self).__init__(*args, **kw)
78 78
79 79 # FrontendWidget protected variables.
80 80 self._call_tip_widget = CallTipWidget(self._control)
81 81 self._completion_lexer = CompletionLexer(PythonLexer())
82 82 self._hidden = True
83 83 self._highlighter = FrontendHighlighter(self)
84 84 self._input_splitter = InputSplitter(input_mode='replace')
85 85 self._kernel_manager = None
86 86
87 87 # Configure the ConsoleWidget.
88 88 self.tab_width = 4
89 89 self._set_continuation_prompt('... ')
90 90
91 91 # Connect signal handlers.
92 92 document = self._control.document()
93 93 document.contentsChange.connect(self._document_contents_change)
94 94
95 95 #---------------------------------------------------------------------------
96 96 # 'ConsoleWidget' abstract interface
97 97 #---------------------------------------------------------------------------
98 98
99 99 def _is_complete(self, source, interactive):
100 100 """ Returns whether 'source' can be completely processed and a new
101 101 prompt created. When triggered by an Enter/Return key press,
102 102 'interactive' is True; otherwise, it is False.
103 103 """
104 104 complete = self._input_splitter.push(source.expandtabs(4))
105 105 if interactive:
106 106 complete = not self._input_splitter.push_accepts_more()
107 107 return complete
108 108
109 109 def _execute(self, source, hidden):
110 110 """ Execute 'source'. If 'hidden', do not show any output.
111 111 """
112 112 self.kernel_manager.xreq_channel.execute(source)
113 113 self._hidden = hidden
114 114
115 115 def _execute_interrupt(self):
116 116 """ Attempts to stop execution. Returns whether this method has an
117 117 implementation.
118 118 """
119 119 self._interrupt_kernel()
120 120 return True
121 121
122 122 def _prompt_started_hook(self):
123 123 """ Called immediately after a new prompt is displayed.
124 124 """
125 125 if not self._reading:
126 126 self._highlighter.highlighting_on = True
127 127
128 128 def _prompt_finished_hook(self):
129 129 """ Called immediately after a prompt is finished, i.e. when some input
130 130 will be processed and a new prompt displayed.
131 131 """
132 132 if not self._reading:
133 133 self._highlighter.highlighting_on = False
134 134
135 135 def _tab_pressed(self):
136 136 """ Called when the tab key is pressed. Returns whether to continue
137 137 processing the event.
138 138 """
139 139 self._keep_cursor_in_buffer()
140 140 cursor = self._get_cursor()
141 141 return not self._complete()
142 142
143 143 #---------------------------------------------------------------------------
144 144 # 'ConsoleWidget' protected interface
145 145 #---------------------------------------------------------------------------
146 146
147 147 def _show_continuation_prompt(self):
148 148 """ Reimplemented for auto-indentation.
149 149 """
150 150 super(FrontendWidget, self)._show_continuation_prompt()
151 151 spaces = self._input_splitter.indent_spaces
152 152 self._append_plain_text('\t' * (spaces / self.tab_width))
153 153 self._append_plain_text(' ' * (spaces % self.tab_width))
154 154
155 155 #---------------------------------------------------------------------------
156 156 # 'BaseFrontendMixin' abstract interface
157 157 #---------------------------------------------------------------------------
158 158
159 159 def _handle_complete_reply(self, rep):
160 160 """ Handle replies for tab completion.
161 161 """
162 162 cursor = self._get_cursor()
163 163 if rep['parent_header']['msg_id'] == self._complete_id and \
164 164 cursor.position() == self._complete_pos:
165 165 text = '.'.join(self._get_context())
166 166 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
167 167 self._complete_with_items(cursor, rep['content']['matches'])
168 168
169 169 def _handle_execute_reply(self, msg):
170 170 """ Handles replies for code execution.
171 171 """
172 172 if not self._hidden:
173 173 # Make sure that all output from the SUB channel has been processed
174 174 # before writing a new prompt.
175 175 self.kernel_manager.sub_channel.flush()
176 176
177 177 content = msg['content']
178 178 status = content['status']
179 179 if status == 'ok':
180 180 self._process_execute_ok(msg)
181 181 elif status == 'error':
182 182 self._process_execute_error(msg)
183 183 elif status == 'abort':
184 184 self._process_execute_abort(msg)
185 185
186 186 self._hidden = True
187 self._show_interpreter_prompt()
187 self._show_interpreter_prompt_for_reply(msg)
188 188 self.executed.emit(msg)
189 189
190 190 def _handle_input_request(self, msg):
191 191 """ Handle requests for raw_input.
192 192 """
193 193 if self._hidden:
194 194 raise RuntimeError('Request for raw input during hidden execution.')
195 195
196 196 # Make sure that all output from the SUB channel has been processed
197 197 # before entering readline mode.
198 198 self.kernel_manager.sub_channel.flush()
199 199
200 200 def callback(line):
201 201 self.kernel_manager.rep_channel.input(line)
202 202 self._readline(msg['content']['prompt'], callback=callback)
203 203
204 204 def _handle_object_info_reply(self, rep):
205 205 """ Handle replies for call tips.
206 206 """
207 207 cursor = self._get_cursor()
208 208 if rep['parent_header']['msg_id'] == self._call_tip_id and \
209 209 cursor.position() == self._call_tip_pos:
210 210 doc = rep['content']['docstring']
211 211 if doc:
212 212 self._call_tip_widget.show_docstring(doc)
213 213
214 214 def _handle_pyout(self, msg):
215 215 """ Handle display hook output.
216 216 """
217 217 self._append_plain_text(msg['content']['data'] + '\n')
218 218
219 219 def _handle_stream(self, msg):
220 220 """ Handle stdout, stderr, and stdin.
221 221 """
222 222 self._append_plain_text(msg['content']['data'])
223 223 self._control.moveCursor(QtGui.QTextCursor.End)
224 224
225 225 def _started_channels(self):
226 226 """ Called when the KernelManager channels have started listening or
227 227 when the frontend is assigned an already listening KernelManager.
228 228 """
229 229 self._reset()
230 230 self._append_plain_text(self._get_banner())
231 231 self._show_interpreter_prompt()
232 232
233 233 def _stopped_channels(self):
234 234 """ Called when the KernelManager channels have stopped listening or
235 235 when a listening KernelManager is removed from the frontend.
236 236 """
237 237 # FIXME: Print a message here?
238 238 pass
239 239
240 240 #---------------------------------------------------------------------------
241 241 # 'FrontendWidget' interface
242 242 #---------------------------------------------------------------------------
243 243
244 244 def execute_file(self, path, hidden=False):
245 245 """ Attempts to execute file with 'path'. If 'hidden', no output is
246 246 shown.
247 247 """
248 248 self.execute('execfile("%s")' % path, hidden=hidden)
249 249
250 250 #---------------------------------------------------------------------------
251 251 # 'FrontendWidget' protected interface
252 252 #---------------------------------------------------------------------------
253 253
254 254 def _call_tip(self):
255 255 """ Shows a call tip, if appropriate, at the current cursor location.
256 256 """
257 257 # Decide if it makes sense to show a call tip
258 258 cursor = self._get_cursor()
259 259 cursor.movePosition(QtGui.QTextCursor.Left)
260 260 document = self._control.document()
261 261 if document.characterAt(cursor.position()).toAscii() != '(':
262 262 return False
263 263 context = self._get_context(cursor)
264 264 if not context:
265 265 return False
266 266
267 267 # Send the metadata request to the kernel
268 268 name = '.'.join(context)
269 269 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
270 270 self._call_tip_pos = self._get_cursor().position()
271 271 return True
272 272
273 273 def _complete(self):
274 274 """ Performs completion at the current cursor location.
275 275 """
276 276 # Decide if it makes sense to do completion
277 277 context = self._get_context()
278 278 if not context:
279 279 return False
280 280
281 281 # Send the completion request to the kernel
282 282 text = '.'.join(context)
283 283 self._complete_id = self.kernel_manager.xreq_channel.complete(
284 284 text, self._get_input_buffer_cursor_line(), self.input_buffer)
285 285 self._complete_pos = self._get_cursor().position()
286 286 return True
287 287
288 288 def _get_banner(self):
289 289 """ Gets a banner to display at the beginning of a session.
290 290 """
291 291 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
292 292 '"license" for more information.'
293 293 return banner % (sys.version, sys.platform)
294 294
295 295 def _get_context(self, cursor=None):
296 296 """ Gets the context at the current cursor location.
297 297 """
298 298 if cursor is None:
299 299 cursor = self._get_cursor()
300 300 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
301 301 QtGui.QTextCursor.KeepAnchor)
302 302 text = str(cursor.selection().toPlainText())
303 303 return self._completion_lexer.get_context(text)
304 304
305 305 def _interrupt_kernel(self):
306 306 """ Attempts to the interrupt the kernel.
307 307 """
308 308 if self.kernel_manager.has_kernel:
309 309 self.kernel_manager.signal_kernel(signal.SIGINT)
310 310 else:
311 311 self._append_plain_text('Kernel process is either remote or '
312 312 'unspecified. Cannot interrupt.\n')
313 313
314 314 def _process_execute_abort(self, msg):
315 315 """ Process a reply for an aborted execution request.
316 316 """
317 317 self._append_plain_text("ERROR: execution aborted\n")
318 318
319 319 def _process_execute_error(self, msg):
320 320 """ Process a reply for an execution request that resulted in an error.
321 321 """
322 322 content = msg['content']
323 323 traceback = ''.join(content['traceback'])
324 324 self._append_plain_text(traceback)
325 325
326 326 def _process_execute_ok(self, msg):
327 327 """ Process a reply for a successful execution equest.
328 328 """
329 329 # The basic FrontendWidget doesn't handle payloads, as they are a
330 330 # mechanism for going beyond the standard Python interpreter model.
331 331 pass
332 332
333 333 def _show_interpreter_prompt(self):
334 334 """ Shows a prompt for the interpreter.
335 335 """
336 336 self._show_prompt('>>> ')
337 337
338 def _show_interpreter_prompt_for_reply(self, msg):
339 """ Shows a prompt for the interpreter given an 'execute_reply' message.
340 """
341 self._show_interpreter_prompt()
342
338 343 #------ Signal handlers ----------------------------------------------------
339 344
340 345 def _document_contents_change(self, position, removed, added):
341 346 """ Called whenever the document's content changes. Display a call tip
342 347 if appropriate.
343 348 """
344 349 # Calculate where the cursor should be *after* the change:
345 350 position += added
346 351
347 352 document = self._control.document()
348 353 if position == self._get_cursor().position():
349 354 self._call_tip()
@@ -1,248 +1,260 b''
1 1 # Standard library imports
2 2 from subprocess import Popen
3 3
4 4 # System library imports
5 5 from PyQt4 import QtCore, QtGui
6 6
7 7 # Local imports
8 8 from IPython.core.inputsplitter import IPythonInputSplitter
9 9 from IPython.core.usage import default_banner
10 10 from frontend_widget import FrontendWidget
11 11
12 12
13 class IPythonPromptBlock(object):
14 """ An internal storage object for IPythonWidget.
15 """
16 def __init__(self, block, length, number):
17 self.block = block
18 self.length = length
19 self.number = number
20
21
13 22 class IPythonWidget(FrontendWidget):
14 23 """ A FrontendWidget for an IPython kernel.
15 24 """
16 25
17 26 # Signal emitted when an editor is needed for a file and the editor has been
18 27 # specified as 'custom'.
19 28 custom_edit_requested = QtCore.pyqtSignal(object)
20 29
21 30 # The default stylesheet: black text on a white background.
22 31 default_stylesheet = """
23 32 .error { color: red; }
24 33 .in-prompt { color: navy; }
25 34 .in-prompt-number { font-weight: bold; }
26 35 .out-prompt { color: darkred; }
27 36 .out-prompt-number { font-weight: bold; }
28 37 """
29 38
30 39 # A dark stylesheet: white text on a black background.
31 40 dark_stylesheet = """
32 41 QPlainTextEdit { background-color: black; color: white }
33 42 QFrame { border: 1px solid grey; }
34 43 .error { color: red; }
35 44 .in-prompt { color: lime; }
36 45 .in-prompt-number { color: lime; font-weight: bold; }
37 46 .out-prompt { color: red; }
38 47 .out-prompt-number { color: red; font-weight: bold; }
39 48 """
40 49
41 50 # Default prompts.
42 51 in_prompt = '<br/>In [<span class="in-prompt-number">%i</span>]: '
43 52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
44 53
45 54 #---------------------------------------------------------------------------
46 55 # 'object' interface
47 56 #---------------------------------------------------------------------------
48 57
49 58 def __init__(self, *args, **kw):
50 59 super(IPythonWidget, self).__init__(*args, **kw)
51 60
52 61 # FrontendWidget protected variables.
53 62 #self._input_splitter = IPythonInputSplitter(input_mode='replace')
54 63
55 64 # IPythonWidget protected variables.
56 self._previous_prompt_blocks = []
57 self._prompt_count = 0
65 self._previous_prompt_obj = None
58 66
59 67 # Set a default editor and stylesheet.
60 68 self.set_editor('default')
61 69 self.reset_styling()
62 70
63 71 #---------------------------------------------------------------------------
64 72 # 'BaseFrontendMixin' abstract interface
65 73 #---------------------------------------------------------------------------
66 74
67 75 def _handle_pyout(self, omsg):
68 76 """ Reimplemented for IPython-style "display hook".
69 77 """
70 78 prompt_number = omsg['content']['prompt_number']
71 79 data = omsg['content']['data']
72 80 self._append_html(self._make_out_prompt(prompt_number))
73 self._save_prompt_block()
74 81 self._append_plain_text(data + '\n')
75 82
76 83 #---------------------------------------------------------------------------
77 84 # 'FrontendWidget' interface
78 85 #---------------------------------------------------------------------------
79 86
80 87 def execute_file(self, path, hidden=False):
81 88 """ Reimplemented to use the 'run' magic.
82 89 """
83 90 self.execute('run %s' % path, hidden=hidden)
84 91
85 92 #---------------------------------------------------------------------------
86 93 # 'FrontendWidget' protected interface
87 94 #---------------------------------------------------------------------------
88 95
89 96 def _get_banner(self):
90 97 """ Reimplemented to return IPython's default banner.
91 98 """
92 99 return default_banner
93 100
94 101 def _process_execute_error(self, msg):
95 102 """ Reimplemented for IPython-style traceback formatting.
96 103 """
97 104 content = msg['content']
98 105 traceback_lines = content['traceback'][:]
99 106 traceback = ''.join(traceback_lines)
100 107 traceback = traceback.replace(' ', '&nbsp;')
101 108 traceback = traceback.replace('\n', '<br/>')
102 109
103 110 ename = content['ename']
104 111 ename_styled = '<span class="error">%s</span>' % ename
105 112 traceback = traceback.replace(ename, ename_styled)
106 113
107 114 self._append_html(traceback)
108 115
109 def _show_interpreter_prompt(self):
116 def _show_interpreter_prompt(self, number=None):
110 117 """ Reimplemented for IPython-style prompts.
111 118 """
112 # Update old prompt numbers if necessary.
113 previous_prompt_number = self._prompt_count
114 if previous_prompt_number != self._prompt_count:
115 for i, (block, length) in enumerate(self._previous_prompt_blocks):
116 if block.isValid():
117 cursor = QtGui.QTextCursor(block)
118 cursor.movePosition(QtGui.QTextCursor.Right,
119 QtGui.QTextCursor.KeepAnchor, length-1)
120 if i == 0:
121 prompt = self._make_in_prompt(previous_prompt_number)
122 else:
123 prompt = self._make_out_prompt(previous_prompt_number)
124 self._insert_html(cursor, prompt)
125 self._previous_prompt_blocks = []
126
127 # Show a new prompt.
128 self._prompt_count += 1
129 self._show_prompt(self._make_in_prompt(self._prompt_count), html=True)
130 self._save_prompt_block()
119 # TODO: If a number was not specified, make a prompt number request.
120 if number is None:
121 number = 0
122
123 # Show a new prompt and save information about it so it can be updated
124 # later if its number needs to be re-written.
125 block = self._control.document().lastBlock()
126 self._show_prompt(self._make_in_prompt(number), html=True)
127 self._previous_prompt_obj = IPythonPromptBlock(
128 block.next(), len(self._prompt), number)
131 129
132 130 # Update continuation prompt to reflect (possibly) new prompt length.
133 131 self._set_continuation_prompt(
134 132 self._make_continuation_prompt(self._prompt), html=True)
135 133
134 def _show_interpreter_prompt_for_reply(self, msg):
135 """ Reimplemented for IPython-style prompts.
136 """
137 # Update the old prompt number if necessary.
138 content = msg['content']
139 previous_prompt_number = content['prompt_number']
140 if self._previous_prompt_obj and \
141 self._previous_prompt_obj.number != previous_prompt_number:
142 block = self._previous_prompt_obj.block
143 if block.isValid():
144 cursor = QtGui.QTextCursor(block)
145 cursor.movePosition(QtGui.QTextCursor.Right,
146 QtGui.QTextCursor.KeepAnchor,
147 self._previous_prompt_obj.length)
148 prompt = self._make_in_prompt(previous_prompt_number)
149 self._insert_html(cursor, prompt)
150 self._previous_prompt_obj = None
151
152 # Show a new prompt with the kernel's estimated prompt number.
153 self._show_interpreter_prompt(content['next_prompt']['prompt_number'])
154
136 155 #---------------------------------------------------------------------------
137 156 # 'IPythonWidget' interface
138 157 #---------------------------------------------------------------------------
139 158
140 159 def edit(self, filename):
141 160 """ Opens a Python script for editing.
142 161
143 162 Parameters:
144 163 -----------
145 164 filename : str
146 165 A path to a local system file.
147 166
148 167 Raises:
149 168 -------
150 169 OSError
151 170 If the editor command cannot be executed.
152 171 """
153 172 if self._editor == 'default':
154 173 url = QtCore.QUrl.fromLocalFile(filename)
155 174 if not QtGui.QDesktopServices.openUrl(url):
156 175 message = 'Failed to open %s with the default application'
157 176 raise OSError(message % repr(filename))
158 177 elif self._editor is None:
159 178 self.custom_edit_requested.emit(filename)
160 179 else:
161 180 Popen(self._editor + [filename])
162 181
163 182 def reset_styling(self):
164 183 """ Restores the default IPythonWidget styling.
165 184 """
166 185 self.set_styling(self.default_stylesheet, syntax_style='default')
167 186 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
168 187
169 188 def set_editor(self, editor):
170 189 """ Sets the editor to use with the %edit magic.
171 190
172 191 Parameters:
173 192 -----------
174 193 editor : str or sequence of str
175 194 A command suitable for use with Popen. This command will be executed
176 195 with a single argument--a filename--when editing is requested.
177 196
178 197 This parameter also takes two special values:
179 198 'default' : Files will be edited with the system default
180 199 application for Python files.
181 200 'custom' : Emit a 'custom_edit_requested(str)' signal instead
182 201 of opening an editor.
183 202 """
184 203 if editor == 'default':
185 204 self._editor = 'default'
186 205 elif editor == 'custom':
187 206 self._editor = None
188 207 elif isinstance(editor, basestring):
189 208 self._editor = [ editor ]
190 209 else:
191 210 self._editor = list(editor)
192 211
193 212 def set_styling(self, stylesheet, syntax_style=None):
194 213 """ Sets the IPythonWidget styling.
195 214
196 215 Parameters:
197 216 -----------
198 217 stylesheet : str
199 218 A CSS stylesheet. The stylesheet can contain classes for:
200 219 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
201 220 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
202 221 3. IPython: .error, .in-prompt, .out-prompt, etc.
203 222
204 223 syntax_style : str or None [default None]
205 224 If specified, use the Pygments style with given name. Otherwise,
206 225 the stylesheet is queried for Pygments style information.
207 226 """
208 227 self.setStyleSheet(stylesheet)
209 228 self._control.document().setDefaultStyleSheet(stylesheet)
210 229 if self._page_control:
211 230 self._page_control.document().setDefaultStyleSheet(stylesheet)
212 231
213 232 if syntax_style is None:
214 233 self._highlighter.set_style_sheet(stylesheet)
215 234 else:
216 235 self._highlighter.set_style(syntax_style)
217 236
218 237 #---------------------------------------------------------------------------
219 238 # 'IPythonWidget' protected interface
220 239 #---------------------------------------------------------------------------
221 240
222 241 def _make_in_prompt(self, number):
223 242 """ Given a prompt number, returns an HTML In prompt.
224 243 """
225 244 body = self.in_prompt % number
226 245 return '<span class="in-prompt">%s</span>' % body
227 246
228 247 def _make_continuation_prompt(self, prompt):
229 248 """ Given a plain text version of an In prompt, returns an HTML
230 249 continuation prompt.
231 250 """
232 251 end_chars = '...: '
233 252 space_count = len(prompt.lstrip('\n')) - len(end_chars)
234 253 body = '&nbsp;' * space_count + end_chars
235 254 return '<span class="in-prompt">%s</span>' % body
236 255
237 256 def _make_out_prompt(self, number):
238 257 """ Given a prompt number, returns an HTML Out prompt.
239 258 """
240 259 body = self.out_prompt % number
241 260 return '<span class="out-prompt">%s</span>' % body
242
243 def _save_prompt_block(self):
244 """ Assuming a prompt has just been written at the end of the buffer,
245 store the QTextBlock that contains it and its length.
246 """
247 block = self._control.document().lastBlock()
248 self._previous_prompt_blocks.append((block, block.length()))
General Comments 0
You need to be logged in to leave comments. Login now