##// END OF EJS Templates
copy pager also hsplit, vsplit...
Matthias BUSSONNIER -
Show More
@@ -1,569 +1,569 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
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 import time
16 16 from textwrap import dedent
17 17
18 18 # System library imports
19 19 from IPython.external.qt import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 23 transform_ipy_prompt
24 24 from IPython.utils.traitlets import Bool, Unicode
25 25 from frontend_widget import FrontendWidget
26 26 import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 65 {filename} format specifier, it will be used. Otherwise, the filename
66 66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 73 this parameter is not specified, the line number option to the %edit
74 74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85 syntax_style = Unicode(config=True,
86 86 help="""
87 87 If not empty, use this Pygments style for syntax highlighting.
88 88 Otherwise, the style sheet is queried for Pygments style
89 89 information.
90 90 """)
91 91
92 92 # Prompts.
93 93 in_prompt = Unicode(default_in_prompt, config=True)
94 94 out_prompt = Unicode(default_out_prompt, config=True)
95 95 input_sep = Unicode(default_input_sep, config=True)
96 96 output_sep = Unicode(default_output_sep, config=True)
97 97 output_sep2 = Unicode(default_output_sep2, config=True)
98 98
99 99 # FrontendWidget protected class variables.
100 100 _input_splitter_class = IPythonInputSplitter
101 101
102 102 # IPythonWidget protected class variables.
103 103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 104 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 105 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 107 _payload_source_page = 'IPython.zmq.page.page'
108 108 _retrying_history_request = False
109 109
110 110 #---------------------------------------------------------------------------
111 111 # 'object' interface
112 112 #---------------------------------------------------------------------------
113 113
114 114 def __init__(self, *args, **kw):
115 115 super(IPythonWidget, self).__init__(*args, **kw)
116 116
117 117 # IPythonWidget protected variables.
118 118 self._payload_handlers = {
119 119 self._payload_source_edit : self._handle_payload_edit,
120 120 self._payload_source_exit : self._handle_payload_exit,
121 121 self._payload_source_page : self._handle_payload_page,
122 122 self._payload_source_next_input : self._handle_payload_next_input }
123 123 self._previous_prompt_obj = None
124 124 self._keep_kernel_on_exit = None
125 125
126 126 # Initialize widget styling.
127 127 if self.style_sheet:
128 128 self._style_sheet_changed()
129 129 self._syntax_style_changed()
130 130 else:
131 131 self.set_default_style()
132 132
133 133 #---------------------------------------------------------------------------
134 134 # 'BaseFrontendMixin' abstract interface
135 135 #---------------------------------------------------------------------------
136 136
137 137 def _handle_complete_reply(self, rep):
138 138 """ Reimplemented to support IPython's improved completion machinery.
139 139 """
140 140 self.log.debug("complete: %s", rep.get('content', ''))
141 141 cursor = self._get_cursor()
142 142 info = self._request_info.get('complete')
143 143 if info and info.id == rep['parent_header']['msg_id'] and \
144 144 info.pos == cursor.position():
145 145 matches = rep['content']['matches']
146 146 text = rep['content']['matched_text']
147 147 offset = len(text)
148 148
149 149 # Clean up matches with period and path separators if the matched
150 150 # text has not been transformed. This is done by truncating all
151 151 # but the last component and then suitably decreasing the offset
152 152 # between the current cursor position and the start of completion.
153 153 if len(matches) > 1 and matches[0][:offset] == text:
154 154 parts = re.split(r'[./\\]', text)
155 155 sep_count = len(parts) - 1
156 156 if sep_count:
157 157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 158 matches = [ match[chop_length:] for match in matches ]
159 159 offset -= chop_length
160 160
161 161 # Move the cursor to the start of the match and complete.
162 162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 163 self._complete_with_items(cursor, matches)
164 164
165 165 def _handle_execute_reply(self, msg):
166 166 """ Reimplemented to support prompt requests.
167 167 """
168 168 msg_id = msg['parent_header'].get('msg_id')
169 169 info = self._request_info['execute'].get(msg_id)
170 170 if info and info.kind == 'prompt':
171 171 number = msg['content']['execution_count'] + 1
172 172 self._show_interpreter_prompt(number)
173 173 self._request_info['execute'].pop(msg_id)
174 174 else:
175 175 super(IPythonWidget, self)._handle_execute_reply(msg)
176 176
177 177 def _handle_history_reply(self, msg):
178 178 """ Implemented to handle history tail replies, which are only supported
179 179 by the IPython kernel.
180 180 """
181 181 self.log.debug("history: %s", msg.get('content', ''))
182 182 content = msg['content']
183 183 if 'history' not in content:
184 184 self.log.error("History request failed: %r"%content)
185 185 if content.get('status', '') == 'aborted' and \
186 186 not self._retrying_history_request:
187 187 # a *different* action caused this request to be aborted, so
188 188 # we should try again.
189 189 self.log.error("Retrying aborted history request")
190 190 # prevent multiple retries of aborted requests:
191 191 self._retrying_history_request = True
192 192 # wait out the kernel's queue flush, which is currently timed at 0.1s
193 193 time.sleep(0.25)
194 194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
195 195 else:
196 196 self._retrying_history_request = False
197 197 return
198 198 # reset retry flag
199 199 self._retrying_history_request = False
200 200 history_items = content['history']
201 201 items = []
202 202 last_cell = u""
203 203 for _, _, cell in history_items:
204 204 cell = cell.rstrip()
205 205 if cell != last_cell:
206 206 items.append(cell)
207 207 last_cell = cell
208 208 self._set_history(items)
209 209
210 210 def _handle_pyout(self, msg):
211 211 """ Reimplemented for IPython-style "display hook".
212 212 """
213 213 self.log.debug("pyout: %s", msg.get('content', ''))
214 214 if not self._hidden and self._is_from_this_session(msg):
215 215 content = msg['content']
216 216 prompt_number = content['execution_count']
217 217 data = content['data']
218 218 if data.has_key('text/html'):
219 219 self._append_plain_text(self.output_sep, True)
220 220 self._append_html(self._make_out_prompt(prompt_number), True)
221 221 html = data['text/html']
222 222 self._append_plain_text('\n', True)
223 223 self._append_html(html + self.output_sep2, True)
224 224 elif data.has_key('text/plain'):
225 225 self._append_plain_text(self.output_sep, True)
226 226 self._append_html(self._make_out_prompt(prompt_number), True)
227 227 text = data['text/plain']
228 228 # If the repr is multiline, make sure we start on a new line,
229 229 # so that its lines are aligned.
230 230 if "\n" in text and not self.output_sep.endswith("\n"):
231 231 self._append_plain_text('\n', True)
232 232 self._append_plain_text(text + self.output_sep2, True)
233 233
234 234 def _handle_display_data(self, msg):
235 235 """ The base handler for the ``display_data`` message.
236 236 """
237 237 self.log.debug("display: %s", msg.get('content', ''))
238 238 # For now, we don't display data from other frontends, but we
239 239 # eventually will as this allows all frontends to monitor the display
240 240 # data. But we need to figure out how to handle this in the GUI.
241 241 if not self._hidden and self._is_from_this_session(msg):
242 242 source = msg['content']['source']
243 243 data = msg['content']['data']
244 244 metadata = msg['content']['metadata']
245 245 # In the regular IPythonWidget, we simply print the plain text
246 246 # representation.
247 247 if data.has_key('text/html'):
248 248 html = data['text/html']
249 249 self._append_html(html, True)
250 250 elif data.has_key('text/plain'):
251 251 text = data['text/plain']
252 252 self._append_plain_text(text, True)
253 253 # This newline seems to be needed for text and html output.
254 254 self._append_plain_text(u'\n', True)
255 255
256 256 def _started_channels(self):
257 257 """ Reimplemented to make a history request.
258 258 """
259 259 super(IPythonWidget, self)._started_channels()
260 260 self.kernel_manager.shell_channel.history(hist_access_type='tail',
261 261 n=1000)
262 262 #---------------------------------------------------------------------------
263 263 # 'ConsoleWidget' public interface
264 264 #---------------------------------------------------------------------------
265 265
266 266 def copy(self):
267 267 """ Copy the currently selected text to the clipboard, removing prompts
268 268 if possible.
269 269 """
270 if self.layout().currentWidget() == self._page_control :
270 if self._page_control.hasFocus() :
271 271 self._page_control.copy()
272 elif self.layout().currentWidget() == self._control :
272 elif self._control.hasFocus() :
273 273 text = self._control.textCursor().selection().toPlainText()
274 274 if text:
275 275 lines = map(transform_ipy_prompt, text.splitlines())
276 276 text = '\n'.join(lines)
277 277 QtGui.QApplication.clipboard().setText(text)
278 278 else :
279 279 self.log.debug("ipython_widget : unknown copy taget")
280 280
281 281
282 282 #---------------------------------------------------------------------------
283 283 # 'FrontendWidget' public interface
284 284 #---------------------------------------------------------------------------
285 285
286 286 def execute_file(self, path, hidden=False):
287 287 """ Reimplemented to use the 'run' magic.
288 288 """
289 289 # Use forward slashes on Windows to avoid escaping each separator.
290 290 if sys.platform == 'win32':
291 291 path = os.path.normpath(path).replace('\\', '/')
292 292
293 293 # Perhaps we should not be using %run directly, but while we
294 294 # are, it is necessary to quote filenames containing spaces or quotes.
295 295 # Escaping quotes in filename in %run seems tricky and inconsistent,
296 296 # so not trying it at present.
297 297 if '"' in path:
298 298 if "'" in path:
299 299 raise ValueError("Can't run filename containing both single "
300 300 "and double quotes: %s" % path)
301 301 path = "'%s'" % path
302 302 elif ' ' in path or "'" in path:
303 303 path = '"%s"' % path
304 304
305 305 self.execute('%%run %s' % path, hidden=hidden)
306 306
307 307 #---------------------------------------------------------------------------
308 308 # 'FrontendWidget' protected interface
309 309 #---------------------------------------------------------------------------
310 310
311 311 def _complete(self):
312 312 """ Reimplemented to support IPython's improved completion machinery.
313 313 """
314 314 # We let the kernel split the input line, so we *always* send an empty
315 315 # text field. Readline-based frontends do get a real text field which
316 316 # they can use.
317 317 text = ''
318 318
319 319 # Send the completion request to the kernel
320 320 msg_id = self.kernel_manager.shell_channel.complete(
321 321 text, # text
322 322 self._get_input_buffer_cursor_line(), # line
323 323 self._get_input_buffer_cursor_column(), # cursor_pos
324 324 self.input_buffer) # block
325 325 pos = self._get_cursor().position()
326 326 info = self._CompletionRequest(msg_id, pos)
327 327 self._request_info['complete'] = info
328 328
329 329 def _process_execute_error(self, msg):
330 330 """ Reimplemented for IPython-style traceback formatting.
331 331 """
332 332 content = msg['content']
333 333 traceback = '\n'.join(content['traceback']) + '\n'
334 334 if False:
335 335 # FIXME: For now, tracebacks come as plain text, so we can't use
336 336 # the html renderer yet. Once we refactor ultratb to produce
337 337 # properly styled tracebacks, this branch should be the default
338 338 traceback = traceback.replace(' ', '&nbsp;')
339 339 traceback = traceback.replace('\n', '<br/>')
340 340
341 341 ename = content['ename']
342 342 ename_styled = '<span class="error">%s</span>' % ename
343 343 traceback = traceback.replace(ename, ename_styled)
344 344
345 345 self._append_html(traceback)
346 346 else:
347 347 # This is the fallback for now, using plain text with ansi escapes
348 348 self._append_plain_text(traceback)
349 349
350 350 def _process_execute_payload(self, item):
351 351 """ Reimplemented to dispatch payloads to handler methods.
352 352 """
353 353 handler = self._payload_handlers.get(item['source'])
354 354 if handler is None:
355 355 # We have no handler for this type of payload, simply ignore it
356 356 return False
357 357 else:
358 358 handler(item)
359 359 return True
360 360
361 361 def _show_interpreter_prompt(self, number=None):
362 362 """ Reimplemented for IPython-style prompts.
363 363 """
364 364 # If a number was not specified, make a prompt number request.
365 365 if number is None:
366 366 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
367 367 info = self._ExecutionRequest(msg_id, 'prompt')
368 368 self._request_info['execute'][msg_id] = info
369 369 return
370 370
371 371 # Show a new prompt and save information about it so that it can be
372 372 # updated later if the prompt number turns out to be wrong.
373 373 self._prompt_sep = self.input_sep
374 374 self._show_prompt(self._make_in_prompt(number), html=True)
375 375 block = self._control.document().lastBlock()
376 376 length = len(self._prompt)
377 377 self._previous_prompt_obj = self._PromptBlock(block, length, number)
378 378
379 379 # Update continuation prompt to reflect (possibly) new prompt length.
380 380 self._set_continuation_prompt(
381 381 self._make_continuation_prompt(self._prompt), html=True)
382 382
383 383 def _show_interpreter_prompt_for_reply(self, msg):
384 384 """ Reimplemented for IPython-style prompts.
385 385 """
386 386 # Update the old prompt number if necessary.
387 387 content = msg['content']
388 388 # abort replies do not have any keys:
389 389 if content['status'] == 'aborted':
390 390 if self._previous_prompt_obj:
391 391 previous_prompt_number = self._previous_prompt_obj.number
392 392 else:
393 393 previous_prompt_number = 0
394 394 else:
395 395 previous_prompt_number = content['execution_count']
396 396 if self._previous_prompt_obj and \
397 397 self._previous_prompt_obj.number != previous_prompt_number:
398 398 block = self._previous_prompt_obj.block
399 399
400 400 # Make sure the prompt block has not been erased.
401 401 if block.isValid() and block.text():
402 402
403 403 # Remove the old prompt and insert a new prompt.
404 404 cursor = QtGui.QTextCursor(block)
405 405 cursor.movePosition(QtGui.QTextCursor.Right,
406 406 QtGui.QTextCursor.KeepAnchor,
407 407 self._previous_prompt_obj.length)
408 408 prompt = self._make_in_prompt(previous_prompt_number)
409 409 self._prompt = self._insert_html_fetching_plain_text(
410 410 cursor, prompt)
411 411
412 412 # When the HTML is inserted, Qt blows away the syntax
413 413 # highlighting for the line, so we need to rehighlight it.
414 414 self._highlighter.rehighlightBlock(cursor.block())
415 415
416 416 self._previous_prompt_obj = None
417 417
418 418 # Show a new prompt with the kernel's estimated prompt number.
419 419 self._show_interpreter_prompt(previous_prompt_number + 1)
420 420
421 421 #---------------------------------------------------------------------------
422 422 # 'IPythonWidget' interface
423 423 #---------------------------------------------------------------------------
424 424
425 425 def set_default_style(self, colors='lightbg'):
426 426 """ Sets the widget style to the class defaults.
427 427
428 428 Parameters:
429 429 -----------
430 430 colors : str, optional (default lightbg)
431 431 Whether to use the default IPython light background or dark
432 432 background or B&W style.
433 433 """
434 434 colors = colors.lower()
435 435 if colors=='lightbg':
436 436 self.style_sheet = styles.default_light_style_sheet
437 437 self.syntax_style = styles.default_light_syntax_style
438 438 elif colors=='linux':
439 439 self.style_sheet = styles.default_dark_style_sheet
440 440 self.syntax_style = styles.default_dark_syntax_style
441 441 elif colors=='nocolor':
442 442 self.style_sheet = styles.default_bw_style_sheet
443 443 self.syntax_style = styles.default_bw_syntax_style
444 444 else:
445 445 raise KeyError("No such color scheme: %s"%colors)
446 446
447 447 #---------------------------------------------------------------------------
448 448 # 'IPythonWidget' protected interface
449 449 #---------------------------------------------------------------------------
450 450
451 451 def _edit(self, filename, line=None):
452 452 """ Opens a Python script for editing.
453 453
454 454 Parameters:
455 455 -----------
456 456 filename : str
457 457 A path to a local system file.
458 458
459 459 line : int, optional
460 460 A line of interest in the file.
461 461 """
462 462 if self.custom_edit:
463 463 self.custom_edit_requested.emit(filename, line)
464 464 elif not self.editor:
465 465 self._append_plain_text('No default editor available.\n'
466 466 'Specify a GUI text editor in the `IPythonWidget.editor` '
467 467 'configurable to enable the %edit magic')
468 468 else:
469 469 try:
470 470 filename = '"%s"' % filename
471 471 if line and self.editor_line:
472 472 command = self.editor_line.format(filename=filename,
473 473 line=line)
474 474 else:
475 475 try:
476 476 command = self.editor.format()
477 477 except KeyError:
478 478 command = self.editor.format(filename=filename)
479 479 else:
480 480 command += ' ' + filename
481 481 except KeyError:
482 482 self._append_plain_text('Invalid editor command.\n')
483 483 else:
484 484 try:
485 485 Popen(command, shell=True)
486 486 except OSError:
487 487 msg = 'Opening editor with command "%s" failed.\n'
488 488 self._append_plain_text(msg % command)
489 489
490 490 def _make_in_prompt(self, number):
491 491 """ Given a prompt number, returns an HTML In prompt.
492 492 """
493 493 try:
494 494 body = self.in_prompt % number
495 495 except TypeError:
496 496 # allow in_prompt to leave out number, e.g. '>>> '
497 497 body = self.in_prompt
498 498 return '<span class="in-prompt">%s</span>' % body
499 499
500 500 def _make_continuation_prompt(self, prompt):
501 501 """ Given a plain text version of an In prompt, returns an HTML
502 502 continuation prompt.
503 503 """
504 504 end_chars = '...: '
505 505 space_count = len(prompt.lstrip('\n')) - len(end_chars)
506 506 body = '&nbsp;' * space_count + end_chars
507 507 return '<span class="in-prompt">%s</span>' % body
508 508
509 509 def _make_out_prompt(self, number):
510 510 """ Given a prompt number, returns an HTML Out prompt.
511 511 """
512 512 body = self.out_prompt % number
513 513 return '<span class="out-prompt">%s</span>' % body
514 514
515 515 #------ Payload handlers --------------------------------------------------
516 516
517 517 # Payload handlers with a generic interface: each takes the opaque payload
518 518 # dict, unpacks it and calls the underlying functions with the necessary
519 519 # arguments.
520 520
521 521 def _handle_payload_edit(self, item):
522 522 self._edit(item['filename'], item['line_number'])
523 523
524 524 def _handle_payload_exit(self, item):
525 525 self._keep_kernel_on_exit = item['keepkernel']
526 526 self.exit_requested.emit(self)
527 527
528 528 def _handle_payload_next_input(self, item):
529 529 self.input_buffer = dedent(item['text'].rstrip())
530 530
531 531 def _handle_payload_page(self, item):
532 532 # Since the plain text widget supports only a very small subset of HTML
533 533 # and we have no control over the HTML source, we only page HTML
534 534 # payloads in the rich text widget.
535 535 if item['html'] and self.kind == 'rich':
536 536 self._page(item['html'], html=True)
537 537 else:
538 538 self._page(item['text'], html=False)
539 539
540 540 #------ Trait change handlers --------------------------------------------
541 541
542 542 def _style_sheet_changed(self):
543 543 """ Set the style sheets of the underlying widgets.
544 544 """
545 545 self.setStyleSheet(self.style_sheet)
546 546 self._control.document().setDefaultStyleSheet(self.style_sheet)
547 547 if self._page_control:
548 548 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
549 549
550 550 bg_color = self._control.palette().window().color()
551 551 self._ansi_processor.set_background_color(bg_color)
552 552
553 553
554 554 def _syntax_style_changed(self):
555 555 """ Set the style for the syntax highlighter.
556 556 """
557 557 if self._highlighter is None:
558 558 # ignore premature calls
559 559 return
560 560 if self.syntax_style:
561 561 self._highlighter.set_style(self.syntax_style)
562 562 else:
563 563 self._highlighter.set_style_sheet(self.style_sheet)
564 564
565 565 #------ Trait default initializers -----------------------------------------
566 566
567 567 def _banner_default(self):
568 568 from IPython.core.usage import default_gui_banner
569 569 return default_gui_banner
General Comments 0
You need to be logged in to leave comments. Login now