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