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