##// END OF EJS Templates
Add history_tail method to ConsoleWidget for retreiving the local history.
epatters -
Show More
@@ -1,604 +1,604 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import time
7 7
8 8 # System library imports
9 9 from pygments.lexers import PythonLexer
10 10 from IPython.external.qt import QtCore, QtGui
11 11
12 12 # Local imports
13 13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 14 from IPython.core.oinspect import call_tip
15 15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 16 from IPython.utils.traitlets import Bool
17 17 from bracket_matcher import BracketMatcher
18 18 from call_tip_widget import CallTipWidget
19 19 from completion_lexer import CompletionLexer
20 20 from history_console_widget import HistoryConsoleWidget
21 21 from pygments_highlighter import PygmentsHighlighter
22 22
23 23
24 24 class FrontendHighlighter(PygmentsHighlighter):
25 25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 26 prompts.
27 27 """
28 28
29 29 def __init__(self, frontend):
30 30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 31 self._current_offset = 0
32 32 self._frontend = frontend
33 33 self.highlighting_on = False
34 34
35 35 def highlightBlock(self, string):
36 36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 37 """
38 38 if not self.highlighting_on:
39 39 return
40 40
41 41 # The input to this function is a unicode string that may contain
42 42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 43 # the string as plain text so we can compare it.
44 44 current_block = self.currentBlock()
45 45 string = self._frontend._get_block_plain_text(current_block)
46 46
47 47 # Decide whether to check for the regular or continuation prompt.
48 48 if current_block.contains(self._frontend._prompt_pos):
49 49 prompt = self._frontend._prompt
50 50 else:
51 51 prompt = self._frontend._continuation_prompt
52 52
53 53 # Don't highlight the part of the string that contains the prompt.
54 54 if string.startswith(prompt):
55 55 self._current_offset = len(prompt)
56 56 string = string[len(prompt):]
57 57 else:
58 58 self._current_offset = 0
59 59
60 60 PygmentsHighlighter.highlightBlock(self, string)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 PygmentsHighlighter.setFormat(self, start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # An option and corresponding signal for overriding the default kernel
82 82 # interrupt behavior.
83 83 custom_interrupt = Bool(False)
84 84 custom_interrupt_requested = QtCore.Signal()
85 85
86 86 # An option and corresponding signals for overriding the default kernel
87 87 # restart behavior.
88 88 custom_restart = Bool(False)
89 89 custom_restart_kernel_died = QtCore.Signal(float)
90 90 custom_restart_requested = QtCore.Signal()
91
92 # Emitted when an 'execute_reply' has been received from the kernel and
93 # processed by the FrontendWidget.
91
92 # Emitted when a user-visible 'execute_reply' has been received from the
93 # kernel and processed by the FrontendWidget. Contains the response message.
94 94 executed = QtCore.Signal(object)
95 95
96 96 # Emitted when an exit request has been received from the kernel.
97 97 exit_requested = QtCore.Signal()
98 98
99 99 # Protected class variables.
100 100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
101 101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
102 102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
103 103 _input_splitter_class = InputSplitter
104 104 _local_kernel = False
105 105
106 106 #---------------------------------------------------------------------------
107 107 # 'object' interface
108 108 #---------------------------------------------------------------------------
109 109
110 110 def __init__(self, *args, **kw):
111 111 super(FrontendWidget, self).__init__(*args, **kw)
112 112
113 113 # FrontendWidget protected variables.
114 114 self._bracket_matcher = BracketMatcher(self._control)
115 115 self._call_tip_widget = CallTipWidget(self._control)
116 116 self._completion_lexer = CompletionLexer(PythonLexer())
117 117 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
118 118 self._hidden = False
119 119 self._highlighter = FrontendHighlighter(self)
120 120 self._input_splitter = self._input_splitter_class(input_mode='cell')
121 121 self._kernel_manager = None
122 122 self._request_info = {}
123 123
124 124 # Configure the ConsoleWidget.
125 125 self.tab_width = 4
126 126 self._set_continuation_prompt('... ')
127 127
128 128 # Configure the CallTipWidget.
129 129 self._call_tip_widget.setFont(self.font)
130 130 self.font_changed.connect(self._call_tip_widget.setFont)
131 131
132 132 # Configure actions.
133 133 action = self._copy_raw_action
134 134 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
135 135 action.setEnabled(False)
136 136 action.setShortcut(QtGui.QKeySequence(key))
137 137 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
138 138 action.triggered.connect(self.copy_raw)
139 139 self.copy_available.connect(action.setEnabled)
140 140 self.addAction(action)
141 141
142 142 # Connect signal handlers.
143 143 document = self._control.document()
144 144 document.contentsChange.connect(self._document_contents_change)
145 145
146 146 # Set flag for whether we are connected via localhost.
147 147 self._local_kernel = kw.get('local_kernel',
148 148 FrontendWidget._local_kernel)
149 149
150 150 #---------------------------------------------------------------------------
151 151 # 'ConsoleWidget' public interface
152 152 #---------------------------------------------------------------------------
153 153
154 154 def copy(self):
155 155 """ Copy the currently selected text to the clipboard, removing prompts.
156 156 """
157 157 text = self._control.textCursor().selection().toPlainText()
158 158 if text:
159 159 lines = map(transform_classic_prompt, text.splitlines())
160 160 text = '\n'.join(lines)
161 161 QtGui.QApplication.clipboard().setText(text)
162 162
163 163 #---------------------------------------------------------------------------
164 164 # 'ConsoleWidget' abstract interface
165 165 #---------------------------------------------------------------------------
166 166
167 167 def _is_complete(self, source, interactive):
168 168 """ Returns whether 'source' can be completely processed and a new
169 169 prompt created. When triggered by an Enter/Return key press,
170 170 'interactive' is True; otherwise, it is False.
171 171 """
172 172 complete = self._input_splitter.push(source)
173 173 if interactive:
174 174 complete = not self._input_splitter.push_accepts_more()
175 175 return complete
176 176
177 177 def _execute(self, source, hidden):
178 178 """ Execute 'source'. If 'hidden', do not show any output.
179 179
180 180 See parent class :meth:`execute` docstring for full details.
181 181 """
182 182 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
183 183 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
184 184 self._hidden = hidden
185 185
186 186 def _prompt_started_hook(self):
187 187 """ Called immediately after a new prompt is displayed.
188 188 """
189 189 if not self._reading:
190 190 self._highlighter.highlighting_on = True
191 191
192 192 def _prompt_finished_hook(self):
193 193 """ Called immediately after a prompt is finished, i.e. when some input
194 194 will be processed and a new prompt displayed.
195 195 """
196 196 # Flush all state from the input splitter so the next round of
197 197 # reading input starts with a clean buffer.
198 198 self._input_splitter.reset()
199 199
200 200 if not self._reading:
201 201 self._highlighter.highlighting_on = False
202 202
203 203 def _tab_pressed(self):
204 204 """ Called when the tab key is pressed. Returns whether to continue
205 205 processing the event.
206 206 """
207 207 # Perform tab completion if:
208 208 # 1) The cursor is in the input buffer.
209 209 # 2) There is a non-whitespace character before the cursor.
210 210 text = self._get_input_buffer_cursor_line()
211 211 if text is None:
212 212 return False
213 213 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
214 214 if complete:
215 215 self._complete()
216 216 return not complete
217 217
218 218 #---------------------------------------------------------------------------
219 219 # 'ConsoleWidget' protected interface
220 220 #---------------------------------------------------------------------------
221 221
222 222 def _context_menu_make(self, pos):
223 223 """ Reimplemented to add an action for raw copy.
224 224 """
225 225 menu = super(FrontendWidget, self)._context_menu_make(pos)
226 226 for before_action in menu.actions():
227 227 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
228 228 QtGui.QKeySequence.ExactMatch:
229 229 menu.insertAction(before_action, self._copy_raw_action)
230 230 break
231 231 return menu
232 232
233 233 def _event_filter_console_keypress(self, event):
234 234 """ Reimplemented for execution interruption and smart backspace.
235 235 """
236 236 key = event.key()
237 237 if self._control_key_down(event.modifiers(), include_command=False):
238 238
239 239 if key == QtCore.Qt.Key_C and self._executing:
240 240 self.interrupt_kernel()
241 241 return True
242 242
243 243 elif key == QtCore.Qt.Key_Period:
244 244 message = 'Are you sure you want to restart the kernel?'
245 245 self.restart_kernel(message, now=False)
246 246 return True
247 247
248 248 elif not event.modifiers() & QtCore.Qt.AltModifier:
249 249
250 250 # Smart backspace: remove four characters in one backspace if:
251 251 # 1) everything left of the cursor is whitespace
252 252 # 2) the four characters immediately left of the cursor are spaces
253 253 if key == QtCore.Qt.Key_Backspace:
254 254 col = self._get_input_buffer_cursor_column()
255 255 cursor = self._control.textCursor()
256 256 if col > 3 and not cursor.hasSelection():
257 257 text = self._get_input_buffer_cursor_line()[:col]
258 258 if text.endswith(' ') and not text.strip():
259 259 cursor.movePosition(QtGui.QTextCursor.Left,
260 260 QtGui.QTextCursor.KeepAnchor, 4)
261 261 cursor.removeSelectedText()
262 262 return True
263 263
264 264 return super(FrontendWidget, self)._event_filter_console_keypress(event)
265 265
266 266 def _insert_continuation_prompt(self, cursor):
267 267 """ Reimplemented for auto-indentation.
268 268 """
269 269 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
270 270 cursor.insertText(' ' * self._input_splitter.indent_spaces)
271 271
272 272 #---------------------------------------------------------------------------
273 273 # 'BaseFrontendMixin' abstract interface
274 274 #---------------------------------------------------------------------------
275 275
276 276 def _handle_complete_reply(self, rep):
277 277 """ Handle replies for tab completion.
278 278 """
279 279 cursor = self._get_cursor()
280 280 info = self._request_info.get('complete')
281 281 if info and info.id == rep['parent_header']['msg_id'] and \
282 282 info.pos == cursor.position():
283 283 text = '.'.join(self._get_context())
284 284 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
285 285 self._complete_with_items(cursor, rep['content']['matches'])
286 286
287 287 def _handle_execute_reply(self, msg):
288 288 """ Handles replies for code execution.
289 289 """
290 290 info = self._request_info.get('execute')
291 291 if info and info.id == msg['parent_header']['msg_id'] and \
292 292 info.kind == 'user' and not self._hidden:
293 293 # Make sure that all output from the SUB channel has been processed
294 294 # before writing a new prompt.
295 295 self.kernel_manager.sub_channel.flush()
296 296
297 297 # Reset the ANSI style information to prevent bad text in stdout
298 298 # from messing up our colors. We're not a true terminal so we're
299 299 # allowed to do this.
300 300 if self.ansi_codes:
301 301 self._ansi_processor.reset_sgr()
302 302
303 303 content = msg['content']
304 304 status = content['status']
305 305 if status == 'ok':
306 306 self._process_execute_ok(msg)
307 307 elif status == 'error':
308 308 self._process_execute_error(msg)
309 309 elif status == 'abort':
310 310 self._process_execute_abort(msg)
311 311
312 312 self._show_interpreter_prompt_for_reply(msg)
313 313 self.executed.emit(msg)
314 314
315 315 def _handle_input_request(self, msg):
316 316 """ Handle requests for raw_input.
317 317 """
318 318 if self._hidden:
319 319 raise RuntimeError('Request for raw input during hidden execution.')
320 320
321 321 # Make sure that all output from the SUB channel has been processed
322 322 # before entering readline mode.
323 323 self.kernel_manager.sub_channel.flush()
324 324
325 325 def callback(line):
326 326 self.kernel_manager.rep_channel.input(line)
327 327 self._readline(msg['content']['prompt'], callback=callback)
328 328
329 329 def _handle_kernel_died(self, since_last_heartbeat):
330 330 """ Handle the kernel's death by asking if the user wants to restart.
331 331 """
332 332 if self.custom_restart:
333 333 self.custom_restart_kernel_died.emit(since_last_heartbeat)
334 334 else:
335 335 message = 'The kernel heartbeat has been inactive for %.2f ' \
336 336 'seconds. Do you want to restart the kernel? You may ' \
337 337 'first want to check the network connection.' % \
338 338 since_last_heartbeat
339 339 self.restart_kernel(message, now=True)
340 340
341 341 def _handle_object_info_reply(self, rep):
342 342 """ Handle replies for call tips.
343 343 """
344 344 cursor = self._get_cursor()
345 345 info = self._request_info.get('call_tip')
346 346 if info and info.id == rep['parent_header']['msg_id'] and \
347 347 info.pos == cursor.position():
348 348 # Get the information for a call tip. For now we format the call
349 349 # line as string, later we can pass False to format_call and
350 350 # syntax-highlight it ourselves for nicer formatting in the
351 351 # calltip.
352 352 call_info, doc = call_tip(rep['content'], format_call=True)
353 353 if call_info or doc:
354 354 self._call_tip_widget.show_call_info(call_info, doc)
355 355
356 356 def _handle_pyout(self, msg):
357 357 """ Handle display hook output.
358 358 """
359 359 if not self._hidden and self._is_from_this_session(msg):
360 360 self._append_plain_text(msg['content']['data']['text/plain'] + '\n')
361 361
362 362 def _handle_stream(self, msg):
363 363 """ Handle stdout, stderr, and stdin.
364 364 """
365 365 if not self._hidden and self._is_from_this_session(msg):
366 366 # Most consoles treat tabs as being 8 space characters. Convert tabs
367 367 # to spaces so that output looks as expected regardless of this
368 368 # widget's tab width.
369 369 text = msg['content']['data'].expandtabs(8)
370 370
371 371 self._append_plain_text(text)
372 372 self._control.moveCursor(QtGui.QTextCursor.End)
373 373
374 374 def _handle_shutdown_reply(self, msg):
375 375 """ Handle shutdown signal, only if from other console.
376 376 """
377 377 if not self._hidden and not self._is_from_this_session(msg):
378 378 if self._local_kernel:
379 379 if not msg['content']['restart']:
380 380 sys.exit(0)
381 381 else:
382 382 # we just got notified of a restart!
383 383 time.sleep(0.25) # wait 1/4 sec to reset
384 384 # lest the request for a new prompt
385 385 # goes to the old kernel
386 386 self.reset()
387 387 else: # remote kernel, prompt on Kernel shutdown/reset
388 388 title = self.window().windowTitle()
389 389 if not msg['content']['restart']:
390 390 reply = QtGui.QMessageBox.question(self, title,
391 391 "Kernel has been shutdown permanently. "
392 392 "Close the Console?",
393 393 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
394 394 if reply == QtGui.QMessageBox.Yes:
395 395 sys.exit(0)
396 396 else:
397 397 reply = QtGui.QMessageBox.question(self, title,
398 398 "Kernel has been reset. Clear the Console?",
399 399 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
400 400 if reply == QtGui.QMessageBox.Yes:
401 401 time.sleep(0.25) # wait 1/4 sec to reset
402 402 # lest the request for a new prompt
403 403 # goes to the old kernel
404 404 self.reset()
405 405
406 406 def _started_channels(self):
407 407 """ Called when the KernelManager channels have started listening or
408 408 when the frontend is assigned an already listening KernelManager.
409 409 """
410 410 self.reset()
411 411
412 412 #---------------------------------------------------------------------------
413 413 # 'FrontendWidget' public interface
414 414 #---------------------------------------------------------------------------
415 415
416 416 def copy_raw(self):
417 417 """ Copy the currently selected text to the clipboard without attempting
418 418 to remove prompts or otherwise alter the text.
419 419 """
420 420 self._control.copy()
421 421
422 422 def execute_file(self, path, hidden=False):
423 423 """ Attempts to execute file with 'path'. If 'hidden', no output is
424 424 shown.
425 425 """
426 426 self.execute('execfile("%s")' % path, hidden=hidden)
427 427
428 428 def interrupt_kernel(self):
429 429 """ Attempts to interrupt the running kernel.
430 430 """
431 431 if self.custom_interrupt:
432 432 self.custom_interrupt_requested.emit()
433 433 elif self.kernel_manager.has_kernel:
434 434 self.kernel_manager.interrupt_kernel()
435 435 else:
436 436 self._append_plain_text('Kernel process is either remote or '
437 437 'unspecified. Cannot interrupt.\n')
438 438
439 439 def reset(self):
440 440 """ Resets the widget to its initial state. Similar to ``clear``, but
441 441 also re-writes the banner and aborts execution if necessary.
442 442 """
443 443 if self._executing:
444 444 self._executing = False
445 445 self._request_info['execute'] = None
446 446 self._reading = False
447 447 self._highlighter.highlighting_on = False
448 448
449 449 self._control.clear()
450 450 self._append_plain_text(self._get_banner())
451 451 self._show_interpreter_prompt()
452 452
453 453 def restart_kernel(self, message, now=False):
454 454 """ Attempts to restart the running kernel.
455 455 """
456 456 # FIXME: now should be configurable via a checkbox in the dialog. Right
457 457 # now at least the heartbeat path sets it to True and the manual restart
458 458 # to False. But those should just be the pre-selected states of a
459 459 # checkbox that the user could override if so desired. But I don't know
460 460 # enough Qt to go implementing the checkbox now.
461 461
462 462 if self.custom_restart:
463 463 self.custom_restart_requested.emit()
464 464
465 465 elif self.kernel_manager.has_kernel:
466 466 # Pause the heart beat channel to prevent further warnings.
467 467 self.kernel_manager.hb_channel.pause()
468 468
469 469 # Prompt the user to restart the kernel. Un-pause the heartbeat if
470 470 # they decline. (If they accept, the heartbeat will be un-paused
471 471 # automatically when the kernel is restarted.)
472 472 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
473 473 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
474 474 message, buttons)
475 475 if result == QtGui.QMessageBox.Yes:
476 476 try:
477 477 self.kernel_manager.restart_kernel(now=now)
478 478 except RuntimeError:
479 479 self._append_plain_text('Kernel started externally. '
480 480 'Cannot restart.\n')
481 481 else:
482 482 self.reset()
483 483 else:
484 484 self.kernel_manager.hb_channel.unpause()
485 485
486 486 else:
487 487 self._append_plain_text('Kernel process is either remote or '
488 488 'unspecified. Cannot restart.\n')
489 489
490 490 #---------------------------------------------------------------------------
491 491 # 'FrontendWidget' protected interface
492 492 #---------------------------------------------------------------------------
493 493
494 494 def _call_tip(self):
495 495 """ Shows a call tip, if appropriate, at the current cursor location.
496 496 """
497 497 # Decide if it makes sense to show a call tip
498 498 cursor = self._get_cursor()
499 499 cursor.movePosition(QtGui.QTextCursor.Left)
500 500 if cursor.document().characterAt(cursor.position()) != '(':
501 501 return False
502 502 context = self._get_context(cursor)
503 503 if not context:
504 504 return False
505 505
506 506 # Send the metadata request to the kernel
507 507 name = '.'.join(context)
508 508 msg_id = self.kernel_manager.xreq_channel.object_info(name)
509 509 pos = self._get_cursor().position()
510 510 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
511 511 return True
512 512
513 513 def _complete(self):
514 514 """ Performs completion at the current cursor location.
515 515 """
516 516 context = self._get_context()
517 517 if context:
518 518 # Send the completion request to the kernel
519 519 msg_id = self.kernel_manager.xreq_channel.complete(
520 520 '.'.join(context), # text
521 521 self._get_input_buffer_cursor_line(), # line
522 522 self._get_input_buffer_cursor_column(), # cursor_pos
523 523 self.input_buffer) # block
524 524 pos = self._get_cursor().position()
525 525 info = self._CompletionRequest(msg_id, pos)
526 526 self._request_info['complete'] = info
527 527
528 528 def _get_banner(self):
529 529 """ Gets a banner to display at the beginning of a session.
530 530 """
531 531 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
532 532 '"license" for more information.'
533 533 return banner % (sys.version, sys.platform)
534 534
535 535 def _get_context(self, cursor=None):
536 536 """ Gets the context for the specified cursor (or the current cursor
537 537 if none is specified).
538 538 """
539 539 if cursor is None:
540 540 cursor = self._get_cursor()
541 541 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
542 542 QtGui.QTextCursor.KeepAnchor)
543 543 text = cursor.selection().toPlainText()
544 544 return self._completion_lexer.get_context(text)
545 545
546 546 def _process_execute_abort(self, msg):
547 547 """ Process a reply for an aborted execution request.
548 548 """
549 549 self._append_plain_text("ERROR: execution aborted\n")
550 550
551 551 def _process_execute_error(self, msg):
552 552 """ Process a reply for an execution request that resulted in an error.
553 553 """
554 554 content = msg['content']
555 555 # If a SystemExit is passed along, this means exit() was called - also
556 556 # all the ipython %exit magic syntax of '-k' to be used to keep
557 557 # the kernel running
558 558 if content['ename']=='SystemExit':
559 559 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
560 560 self._keep_kernel_on_exit = keepkernel
561 561 self.exit_requested.emit()
562 562 else:
563 563 traceback = ''.join(content['traceback'])
564 564 self._append_plain_text(traceback)
565 565
566 566 def _process_execute_ok(self, msg):
567 567 """ Process a reply for a successful execution equest.
568 568 """
569 569 payload = msg['content']['payload']
570 570 for item in payload:
571 571 if not self._process_execute_payload(item):
572 572 warning = 'Warning: received unknown payload of type %s'
573 573 print(warning % repr(item['source']))
574 574
575 575 def _process_execute_payload(self, item):
576 576 """ Process a single payload item from the list of payload items in an
577 577 execution reply. Returns whether the payload was handled.
578 578 """
579 579 # The basic FrontendWidget doesn't handle payloads, as they are a
580 580 # mechanism for going beyond the standard Python interpreter model.
581 581 return False
582 582
583 583 def _show_interpreter_prompt(self):
584 584 """ Shows a prompt for the interpreter.
585 585 """
586 586 self._show_prompt('>>> ')
587 587
588 588 def _show_interpreter_prompt_for_reply(self, msg):
589 589 """ Shows a prompt for the interpreter given an 'execute_reply' message.
590 590 """
591 591 self._show_interpreter_prompt()
592 592
593 593 #------ Signal handlers ----------------------------------------------------
594 594
595 595 def _document_contents_change(self, position, removed, added):
596 596 """ Called whenever the document's content changes. Display a call tip
597 597 if appropriate.
598 598 """
599 599 # Calculate where the cursor should be *after* the change:
600 600 position += added
601 601
602 602 document = self._control.document()
603 603 if position == self._get_cursor().position():
604 604 self._call_tip()
@@ -1,163 +1,173 b''
1 1 # System library imports
2 2 from IPython.external.qt import QtGui
3 3
4 4 # Local imports
5 5 from console_widget import ConsoleWidget
6 6
7 7
8 8 class HistoryConsoleWidget(ConsoleWidget):
9 9 """ A ConsoleWidget that keeps a history of the commands that have been
10 10 executed and provides a readline-esque interface to this history.
11 11 """
12 12
13 13 #---------------------------------------------------------------------------
14 14 # 'object' interface
15 15 #---------------------------------------------------------------------------
16 16
17 17 def __init__(self, *args, **kw):
18 18 super(HistoryConsoleWidget, self).__init__(*args, **kw)
19 19
20 20 # HistoryConsoleWidget protected variables.
21 21 self._history = []
22 22 self._history_index = 0
23 23 self._history_prefix = ''
24 24
25 25 #---------------------------------------------------------------------------
26 26 # 'ConsoleWidget' public interface
27 27 #---------------------------------------------------------------------------
28 28
29 29 def execute(self, source=None, hidden=False, interactive=False):
30 30 """ Reimplemented to the store history.
31 31 """
32 32 if not hidden:
33 33 history = self.input_buffer if source is None else source
34 34
35 35 executed = super(HistoryConsoleWidget, self).execute(
36 36 source, hidden, interactive)
37 37
38 38 if executed and not hidden:
39 39 # Save the command unless it was an empty string or was identical
40 40 # to the previous command.
41 41 history = history.rstrip()
42 42 if history and (not self._history or self._history[-1] != history):
43 43 self._history.append(history)
44 44
45 45 # Move the history index to the most recent item.
46 46 self._history_index = len(self._history)
47 47
48 48 return executed
49 49
50 50 #---------------------------------------------------------------------------
51 51 # 'ConsoleWidget' abstract interface
52 52 #---------------------------------------------------------------------------
53 53
54 54 def _up_pressed(self):
55 55 """ Called when the up key is pressed. Returns whether to continue
56 56 processing the event.
57 57 """
58 58 prompt_cursor = self._get_prompt_cursor()
59 59 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
60 60
61 61 # Set a search prefix based on the cursor position.
62 62 col = self._get_input_buffer_cursor_column()
63 63 input_buffer = self.input_buffer
64 64 if self._history_index == len(self._history) or \
65 65 (self._history_prefix and col != len(self._history_prefix)):
66 66 self._history_index = len(self._history)
67 67 self._history_prefix = input_buffer[:col]
68 68
69 69 # Perform the search.
70 70 self.history_previous(self._history_prefix)
71 71
72 72 # Go to the first line of the prompt for seemless history scrolling.
73 73 # Emulate readline: keep the cursor position fixed for a prefix
74 74 # search.
75 75 cursor = self._get_prompt_cursor()
76 76 if self._history_prefix:
77 77 cursor.movePosition(QtGui.QTextCursor.Right,
78 78 n=len(self._history_prefix))
79 79 else:
80 80 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
81 81 self._set_cursor(cursor)
82 82
83 83 return False
84 84
85 85 return True
86 86
87 87 def _down_pressed(self):
88 88 """ Called when the down key is pressed. Returns whether to continue
89 89 processing the event.
90 90 """
91 91 end_cursor = self._get_end_cursor()
92 92 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
93 93
94 94 # Perform the search.
95 95 self.history_next(self._history_prefix)
96 96
97 97 # Emulate readline: keep the cursor position fixed for a prefix
98 98 # search. (We don't need to move the cursor to the end of the buffer
99 99 # in the other case because this happens automatically when the
100 100 # input buffer is set.)
101 101 if self._history_prefix:
102 102 cursor = self._get_prompt_cursor()
103 103 cursor.movePosition(QtGui.QTextCursor.Right,
104 104 n=len(self._history_prefix))
105 105 self._set_cursor(cursor)
106 106
107 107 return False
108 108
109 109 return True
110 110
111 111 #---------------------------------------------------------------------------
112 112 # 'HistoryConsoleWidget' public interface
113 113 #---------------------------------------------------------------------------
114 114
115 115 def history_previous(self, prefix=''):
116 116 """ If possible, set the input buffer to a previous item in the history.
117 117
118 118 Parameters:
119 119 -----------
120 120 prefix : str, optional
121 121 If specified, search for an item with this prefix.
122 122 """
123 123 index = self._history_index
124 124 while index > 0:
125 125 index -= 1
126 126 history = self._history[index]
127 127 if history.startswith(prefix):
128 128 break
129 129 else:
130 130 history = None
131 131
132 132 if history is not None:
133 133 self._history_index = index
134 134 self.input_buffer = history
135 135
136 136 def history_next(self, prefix=''):
137 137 """ Set the input buffer to a subsequent item in the history, or to the
138 138 original search prefix if there is no such item.
139 139
140 140 Parameters:
141 141 -----------
142 142 prefix : str, optional
143 143 If specified, search for an item with this prefix.
144 144 """
145 145 while self._history_index < len(self._history) - 1:
146 146 self._history_index += 1
147 147 history = self._history[self._history_index]
148 148 if history.startswith(prefix):
149 149 break
150 150 else:
151 151 self._history_index = len(self._history)
152 152 history = prefix
153 153 self.input_buffer = history
154 154
155 def history_tail(self, n=10):
156 """ Get the local history list.
157
158 Parameters
159 ----------
160 n : int
161 The (maximum) number of history items to get.
162 """
163 return self._history[-n:]
164
155 165 #---------------------------------------------------------------------------
156 166 # 'HistoryConsoleWidget' protected interface
157 167 #---------------------------------------------------------------------------
158 168
159 169 def _set_history(self, history):
160 170 """ Replace the current history with a sequence of history items.
161 171 """
162 172 self._history = list(history)
163 173 self._history_index = len(self._history)
General Comments 0
You need to be logged in to leave comments. Login now