##// END OF EJS Templates
Implement support for 'cell' mode with Ctrl-Enter....
Fernando Perez -
Show More
@@ -1,956 +1,977 b''
1 1 """Analysis of text input into executable blocks.
2 2
3 3 The main class in this module, :class:`InputSplitter`, is designed to break
4 4 input from either interactive, line-by-line environments or block-based ones,
5 5 into standalone blocks that can be executed by Python as 'single' statements
6 6 (thus triggering sys.displayhook).
7 7
8 8 A companion, :class:`IPythonInputSplitter`, provides the same functionality but
9 9 with full support for the extended IPython syntax (magics, system calls, etc).
10 10
11 11 For more details, see the class docstring below.
12 12
13 13 Syntax Transformations
14 14 ----------------------
15 15
16 16 One of the main jobs of the code in this file is to apply all syntax
17 17 transformations that make up 'the IPython language', i.e. magics, shell
18 18 escapes, etc. All transformations should be implemented as *fully stateless*
19 19 entities, that simply take one line as their input and return a line.
20 20 Internally for implementation purposes they may be a normal function or a
21 21 callable object, but the only input they receive will be a single line and they
22 22 should only return a line, without holding any data-dependent state between
23 23 calls.
24 24
25 25 As an example, the EscapedTransformer is a class so we can more clearly group
26 26 together the functionality of dispatching to individual functions based on the
27 27 starting escape character, but the only method for public use is its call
28 28 method.
29 29
30 30
31 31 ToDo
32 32 ----
33 33
34 34 - Should we make push() actually raise an exception once push_accepts_more()
35 35 returns False?
36 36
37 37 - Naming cleanups. The tr_* names aren't the most elegant, though now they are
38 38 at least just attributes of a class so not really very exposed.
39 39
40 40 - Think about the best way to support dynamic things: automagic, autocall,
41 41 macros, etc.
42 42
43 43 - Think of a better heuristic for the application of the transforms in
44 44 IPythonInputSplitter.push() than looking at the buffer ending in ':'. Idea:
45 45 track indentation change events (indent, dedent, nothing) and apply them only
46 46 if the indentation went up, but not otherwise.
47 47
48 48 - Think of the cleanest way for supporting user-specified transformations (the
49 49 user prefilters we had before).
50 50
51 51 Authors
52 52 -------
53 53
54 54 * Fernando Perez
55 55 * Brian Granger
56 56 """
57 57 #-----------------------------------------------------------------------------
58 58 # Copyright (C) 2010 The IPython Development Team
59 59 #
60 60 # Distributed under the terms of the BSD License. The full license is in
61 61 # the file COPYING, distributed as part of this software.
62 62 #-----------------------------------------------------------------------------
63 from __future__ import print_function
63 64
64 65 #-----------------------------------------------------------------------------
65 66 # Imports
66 67 #-----------------------------------------------------------------------------
67 68 # stdlib
68 69 import codeop
69 70 import re
70 71 import sys
71 72
72 73 # IPython modules
73 74 from IPython.utils.text import make_quoted_expr
75
74 76 #-----------------------------------------------------------------------------
75 77 # Globals
76 78 #-----------------------------------------------------------------------------
77 79
78 80 # The escape sequences that define the syntax transformations IPython will
79 81 # apply to user input. These can NOT be just changed here: many regular
80 82 # expressions and other parts of the code may use their hardcoded values, and
81 83 # for all intents and purposes they constitute the 'IPython syntax', so they
82 84 # should be considered fixed.
83 85
84 ESC_SHELL = '!'
85 ESC_SH_CAP = '!!'
86 ESC_HELP = '?'
87 ESC_HELP2 = '??'
88 ESC_MAGIC = '%'
89 ESC_QUOTE = ','
90 ESC_QUOTE2 = ';'
91 ESC_PAREN = '/'
86 ESC_SHELL = '!' # Send line to underlying system shell
87 ESC_SH_CAP = '!!' # Send line to system shell and capture output
88 ESC_HELP = '?' # Find information about object
89 ESC_HELP2 = '??' # Find extra-detailed information about object
90 ESC_MAGIC = '%' # Call magic function
91 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
92 ESC_QUOTE2 = ';' # Quote all args as a single string, call
93 ESC_PAREN = '/' # Call first argument with rest of line as arguments
92 94
93 95 #-----------------------------------------------------------------------------
94 96 # Utilities
95 97 #-----------------------------------------------------------------------------
96 98
97 99 # FIXME: These are general-purpose utilities that later can be moved to the
98 100 # general ward. Kept here for now because we're being very strict about test
99 101 # coverage with this code, and this lets us ensure that we keep 100% coverage
100 102 # while developing.
101 103
102 104 # compiled regexps for autoindent management
103 105 dedent_re = re.compile(r'^\s+raise|^\s+return|^\s+pass')
104 106 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
105 107
106 108 # regexp to match pure comment lines so we don't accidentally insert 'if 1:'
107 109 # before pure comments
108 110 comment_line_re = re.compile('^\s*\#')
109 111
110 112
111 113 def num_ini_spaces(s):
112 114 """Return the number of initial spaces in a string.
113 115
114 116 Note that tabs are counted as a single space. For now, we do *not* support
115 117 mixing of tabs and spaces in the user's input.
116 118
117 119 Parameters
118 120 ----------
119 121 s : string
120 122
121 123 Returns
122 124 -------
123 125 n : int
124 126 """
125 127
126 128 ini_spaces = ini_spaces_re.match(s)
127 129 if ini_spaces:
128 130 return ini_spaces.end()
129 131 else:
130 132 return 0
131 133
132 134
133 135 def remove_comments(src):
134 136 """Remove all comments from input source.
135 137
136 138 Note: comments are NOT recognized inside of strings!
137 139
138 140 Parameters
139 141 ----------
140 142 src : string
141 143 A single or multiline input string.
142 144
143 145 Returns
144 146 -------
145 147 String with all Python comments removed.
146 148 """
147 149
148 150 return re.sub('#.*', '', src)
149 151
150 152
151 153 def get_input_encoding():
152 154 """Return the default standard input encoding.
153 155
154 156 If sys.stdin has no encoding, 'ascii' is returned."""
155 157 # There are strange environments for which sys.stdin.encoding is None. We
156 158 # ensure that a valid encoding is returned.
157 159 encoding = getattr(sys.stdin, 'encoding', None)
158 160 if encoding is None:
159 161 encoding = 'ascii'
160 162 return encoding
161 163
162 164 #-----------------------------------------------------------------------------
163 165 # Classes and functions for normal Python syntax handling
164 166 #-----------------------------------------------------------------------------
165 167
166 168 # HACK! This implementation, written by Robert K a while ago using the
167 169 # compiler module, is more robust than the other one below, but it expects its
168 170 # input to be pure python (no ipython syntax). For now we're using it as a
169 171 # second-pass splitter after the first pass transforms the input to pure
170 172 # python.
171 173
172 174 def split_blocks(python):
173 175 """ Split multiple lines of code into discrete commands that can be
174 176 executed singly.
175 177
176 178 Parameters
177 179 ----------
178 180 python : str
179 181 Pure, exec'able Python code.
180 182
181 183 Returns
182 184 -------
183 185 commands : list of str
184 186 Separate commands that can be exec'ed independently.
185 187 """
186 188
187 189 import compiler
188 190
189 191 # compiler.parse treats trailing spaces after a newline as a
190 192 # SyntaxError. This is different than codeop.CommandCompiler, which
191 193 # will compile the trailng spaces just fine. We simply strip any
192 194 # trailing whitespace off. Passing a string with trailing whitespace
193 195 # to exec will fail however. There seems to be some inconsistency in
194 196 # how trailing whitespace is handled, but this seems to work.
195 197 python_ori = python # save original in case we bail on error
196 198 python = python.strip()
197 199
198 200 # The compiler module does not like unicode. We need to convert
199 201 # it encode it:
200 202 if isinstance(python, unicode):
201 203 # Use the utf-8-sig BOM so the compiler detects this a UTF-8
202 204 # encode string.
203 205 python = '\xef\xbb\xbf' + python.encode('utf-8')
204 206
205 207 # The compiler module will parse the code into an abstract syntax tree.
206 208 # This has a bug with str("a\nb"), but not str("""a\nb""")!!!
207 209 try:
208 210 ast = compiler.parse(python)
209 211 except:
210 212 return [python_ori]
211 213
212 214 # Uncomment to help debug the ast tree
213 215 # for n in ast.node:
214 216 # print n.lineno,'->',n
215 217
216 218 # Each separate command is available by iterating over ast.node. The
217 219 # lineno attribute is the line number (1-indexed) beginning the commands
218 220 # suite.
219 221 # lines ending with ";" yield a Discard Node that doesn't have a lineno
220 222 # attribute. These nodes can and should be discarded. But there are
221 223 # other situations that cause Discard nodes that shouldn't be discarded.
222 224 # We might eventually discover other cases where lineno is None and have
223 225 # to put in a more sophisticated test.
224 226 linenos = [x.lineno-1 for x in ast.node if x.lineno is not None]
225 227
226 228 # When we finally get the slices, we will need to slice all the way to
227 229 # the end even though we don't have a line number for it. Fortunately,
228 230 # None does the job nicely.
229 231 linenos.append(None)
230 232
231 233 # Same problem at the other end: sometimes the ast tree has its
232 234 # first complete statement not starting on line 0. In this case
233 235 # we might miss part of it. This fixes ticket 266993. Thanks Gael!
234 236 linenos[0] = 0
235 237
236 238 lines = python.splitlines()
237 239
238 240 # Create a list of atomic commands.
239 241 cmds = []
240 242 for i, j in zip(linenos[:-1], linenos[1:]):
241 243 cmd = lines[i:j]
242 244 if cmd:
243 245 cmds.append('\n'.join(cmd)+'\n')
244 246
245 247 return cmds
246 248
247 249
248 250 class InputSplitter(object):
249 251 """An object that can split Python source input in executable blocks.
250 252
251 253 This object is designed to be used in one of two basic modes:
252 254
253 255 1. By feeding it python source line-by-line, using :meth:`push`. In this
254 256 mode, it will return on each push whether the currently pushed code
255 257 could be executed already. In addition, it provides a method called
256 258 :meth:`push_accepts_more` that can be used to query whether more input
257 259 can be pushed into a single interactive block.
258 260
259 261 2. By calling :meth:`split_blocks` with a single, multiline Python string,
260 262 that is then split into blocks each of which can be executed
261 263 interactively as a single statement.
262 264
263 265 This is a simple example of how an interactive terminal-based client can use
264 266 this tool::
265 267
266 268 isp = InputSplitter()
267 269 while isp.push_accepts_more():
268 270 indent = ' '*isp.indent_spaces
269 271 prompt = '>>> ' + indent
270 272 line = indent + raw_input(prompt)
271 273 isp.push(line)
272 274 print 'Input source was:\n', isp.source_reset(),
273 275 """
274 276 # Number of spaces of indentation computed from input that has been pushed
275 277 # so far. This is the attributes callers should query to get the current
276 278 # indentation level, in order to provide auto-indent facilities.
277 279 indent_spaces = 0
278 280 # String, indicating the default input encoding. It is computed by default
279 281 # at initialization time via get_input_encoding(), but it can be reset by a
280 282 # client with specific knowledge of the encoding.
281 283 encoding = ''
282 284 # String where the current full source input is stored, properly encoded.
283 285 # Reading this attribute is the normal way of querying the currently pushed
284 286 # source code, that has been properly encoded.
285 287 source = ''
286 288 # Code object corresponding to the current source. It is automatically
287 289 # synced to the source, so it can be queried at any time to obtain the code
288 290 # object; it will be None if the source doesn't compile to valid Python.
289 291 code = None
290 292 # Input mode
291 293 input_mode = 'line'
292 294
293 295 # Private attributes
294 296
295 297 # List with lines of input accumulated so far
296 298 _buffer = None
297 299 # Command compiler
298 300 _compile = None
299 301 # Mark when input has changed indentation all the way back to flush-left
300 302 _full_dedent = False
301 303 # Boolean indicating whether the current block is complete
302 304 _is_complete = None
303 305
304 306 def __init__(self, input_mode=None):
305 307 """Create a new InputSplitter instance.
306 308
307 309 Parameters
308 310 ----------
309 311 input_mode : str
310 312
311 One of ['line', 'block']; default is 'line'.
313 One of ['line', 'cell']; default is 'line'.
312 314
313 315 The input_mode parameter controls how new inputs are used when fed via
314 316 the :meth:`push` method:
315 317
316 318 - 'line': meant for line-oriented clients, inputs are appended one at a
317 319 time to the internal buffer and the whole buffer is compiled.
318 320
319 - 'block': meant for clients that can edit multi-line blocks of text at
320 a time. Each new input new input completely replaces all prior
321 inputs. Block mode is thus equivalent to prepending a full reset()
322 to every push() call.
321 - 'cell': meant for clients that can edit multi-line 'cells' of text at
322 a time. A cell can contain one or more blocks that can be compile in
323 'single' mode by Python. In this mode, each new input new input
324 completely replaces all prior inputs. Cell mode is thus equivalent
325 to prepending a full reset() to every push() call.
323 326 """
324 327 self._buffer = []
325 328 self._compile = codeop.CommandCompiler()
326 329 self.encoding = get_input_encoding()
327 330 self.input_mode = InputSplitter.input_mode if input_mode is None \
328 331 else input_mode
329 332
330 333 def reset(self):
331 334 """Reset the input buffer and associated state."""
332 335 self.indent_spaces = 0
333 336 self._buffer[:] = []
334 337 self.source = ''
335 338 self.code = None
336 339 self._is_complete = False
337 340 self._full_dedent = False
338 341
339 342 def source_reset(self):
340 343 """Return the input source and perform a full reset.
341 344 """
342 345 out = self.source
343 346 self.reset()
344 347 return out
345 348
346 349 def push(self, lines):
347 350 """Push one ore more lines of input.
348 351
349 352 This stores the given lines and returns a status code indicating
350 353 whether the code forms a complete Python block or not.
351 354
352 355 Any exceptions generated in compilation are swallowed, but if an
353 356 exception was produced, the method returns True.
354 357
355 358 Parameters
356 359 ----------
357 360 lines : string
358 361 One or more lines of Python input.
359 362
360 363 Returns
361 364 -------
362 365 is_complete : boolean
363 366 True if the current input source (the result of the current input
364 367 plus prior inputs) forms a complete Python execution block. Note that
365 368 this value is also stored as a private attribute (_is_complete), so it
366 369 can be queried at any time.
367 370 """
368 if self.input_mode == 'block':
371 if self.input_mode == 'cell':
369 372 self.reset()
370 373
371 374 # If the source code has leading blanks, add 'if 1:\n' to it
372 375 # this allows execution of indented pasted code. It is tempting
373 376 # to add '\n' at the end of source to run commands like ' a=1'
374 377 # directly, but this fails for more complicated scenarios
375 378
376 379 if not self._buffer and lines[:1] in [' ', '\t'] and \
377 380 not comment_line_re.match(lines):
378 381 lines = 'if 1:\n%s' % lines
379 382
380 383 self._store(lines)
381 384 source = self.source
382 385
383 386 # Before calling _compile(), reset the code object to None so that if an
384 387 # exception is raised in compilation, we don't mislead by having
385 388 # inconsistent code/source attributes.
386 389 self.code, self._is_complete = None, None
387 390
388 391 self._update_indent(lines)
389 392 try:
390 393 self.code = self._compile(source)
391 394 # Invalid syntax can produce any of a number of different errors from
392 395 # inside the compiler, so we have to catch them all. Syntax errors
393 396 # immediately produce a 'ready' block, so the invalid Python can be
394 397 # sent to the kernel for evaluation with possible ipython
395 398 # special-syntax conversion.
396 399 except (SyntaxError, OverflowError, ValueError, TypeError,
397 400 MemoryError):
398 401 self._is_complete = True
399 402 else:
400 403 # Compilation didn't produce any exceptions (though it may not have
401 404 # given a complete code object)
402 405 self._is_complete = self.code is not None
403 406
404 407 return self._is_complete
405 408
406 409 def push_accepts_more(self):
407 410 """Return whether a block of interactive input can accept more input.
408 411
409 412 This method is meant to be used by line-oriented frontends, who need to
410 413 guess whether a block is complete or not based solely on prior and
411 414 current input lines. The InputSplitter considers it has a complete
412 415 interactive block and will not accept more input only when either a
413 416 SyntaxError is raised, or *all* of the following are true:
414 417
415 418 1. The input compiles to a complete statement.
416 419
417 420 2. The indentation level is flush-left (because if we are indented,
418 421 like inside a function definition or for loop, we need to keep
419 422 reading new input).
420 423
421 424 3. There is one extra line consisting only of whitespace.
422 425
423 426 Because of condition #3, this method should be used only by
424 427 *line-oriented* frontends, since it means that intermediate blank lines
425 428 are not allowed in function definitions (or any other indented block).
426 429
427 430 Block-oriented frontends that have a separate keyboard event to
428 431 indicate execution should use the :meth:`split_blocks` method instead.
429 432
430 433 If the current input produces a syntax error, this method immediately
431 434 returns False but does *not* raise the syntax error exception, as
432 435 typically clients will want to send invalid syntax to an execution
433 436 backend which might convert the invalid syntax into valid Python via
434 437 one of the dynamic IPython mechanisms.
435 438 """
436
439
440 # With incomplete input, unconditionally accept more
437 441 if not self._is_complete:
438 442 return True
439 443
444 # If we already have complete input and we're flush left, the answer
445 # depends. In line mode, we're done. But in cell mode, we need to
446 # check how many blocks the input so far compiles into, because if
447 # there's already more than one full independent block of input, then
448 # the client has entered full 'cell' mode and is feeding lines that
449 # each is complete. In this case we should then keep accepting.
450 # The Qt terminal-like console does precisely this, to provide the
451 # convenience of terminal-like input of single expressions, but
452 # allowing the user (with a separate keystroke) to switch to 'cell'
453 # mode and type multiple expressions in one shot.
440 454 if self.indent_spaces==0:
441 return False
442
455 if self.input_mode=='line':
456 return False
457 else:
458 nblocks = len(split_blocks(''.join(self._buffer)))
459 if nblocks==1:
460 return False
461
462 # When input is complete, then termination is marked by an extra blank
463 # line at the end.
443 464 last_line = self.source.splitlines()[-1]
444 465 return bool(last_line and not last_line.isspace())
445 466
446 467 def split_blocks(self, lines):
447 468 """Split a multiline string into multiple input blocks.
448 469
449 470 Note: this method starts by performing a full reset().
450 471
451 472 Parameters
452 473 ----------
453 474 lines : str
454 475 A possibly multiline string.
455 476
456 477 Returns
457 478 -------
458 479 blocks : list
459 480 A list of strings, each possibly multiline. Each string corresponds
460 481 to a single block that can be compiled in 'single' mode (unless it
461 482 has a syntax error)."""
462 483
463 484 # This code is fairly delicate. If you make any changes here, make
464 485 # absolutely sure that you do run the full test suite and ALL tests
465 486 # pass.
466 487
467 488 self.reset()
468 489 blocks = []
469 490
470 491 # Reversed copy so we can use pop() efficiently and consume the input
471 492 # as a stack
472 493 lines = lines.splitlines()[::-1]
473 494 # Outer loop over all input
474 495 while lines:
475 496 #print 'Current lines:', lines # dbg
476 497 # Inner loop to build each block
477 498 while True:
478 499 # Safety exit from inner loop
479 500 if not lines:
480 501 break
481 502 # Grab next line but don't push it yet
482 503 next_line = lines.pop()
483 504 # Blank/empty lines are pushed as-is
484 505 if not next_line or next_line.isspace():
485 506 self.push(next_line)
486 507 continue
487 508
488 509 # Check indentation changes caused by the *next* line
489 510 indent_spaces, _full_dedent = self._find_indent(next_line)
490 511
491 512 # If the next line causes a dedent, it can be for two differnt
492 513 # reasons: either an explicit de-dent by the user or a
493 514 # return/raise/pass statement. These MUST be handled
494 515 # separately:
495 516 #
496 517 # 1. the first case is only detected when the actual explicit
497 518 # dedent happens, and that would be the *first* line of a *new*
498 519 # block. Thus, we must put the line back into the input buffer
499 520 # so that it starts a new block on the next pass.
500 521 #
501 522 # 2. the second case is detected in the line before the actual
502 523 # dedent happens, so , we consume the line and we can break out
503 524 # to start a new block.
504 525
505 526 # Case 1, explicit dedent causes a break.
506 527 # Note: check that we weren't on the very last line, else we'll
507 528 # enter an infinite loop adding/removing the last line.
508 529 if _full_dedent and lines and not next_line.startswith(' '):
509 530 lines.append(next_line)
510 531 break
511 532
512 533 # Otherwise any line is pushed
513 534 self.push(next_line)
514 535
515 536 # Case 2, full dedent with full block ready:
516 537 if _full_dedent or \
517 538 self.indent_spaces==0 and not self.push_accepts_more():
518 539 break
519 540 # Form the new block with the current source input
520 541 blocks.append(self.source_reset())
521 542
522 543 #return blocks
523 544 # HACK!!! Now that our input is in blocks but guaranteed to be pure
524 545 # python syntax, feed it back a second time through the AST-based
525 546 # splitter, which is more accurate than ours.
526 547 return split_blocks(''.join(blocks))
527 548
528 549 #------------------------------------------------------------------------
529 550 # Private interface
530 551 #------------------------------------------------------------------------
531 552
532 553 def _find_indent(self, line):
533 554 """Compute the new indentation level for a single line.
534 555
535 556 Parameters
536 557 ----------
537 558 line : str
538 559 A single new line of non-whitespace, non-comment Python input.
539 560
540 561 Returns
541 562 -------
542 563 indent_spaces : int
543 564 New value for the indent level (it may be equal to self.indent_spaces
544 565 if indentation doesn't change.
545 566
546 567 full_dedent : boolean
547 568 Whether the new line causes a full flush-left dedent.
548 569 """
549 570 indent_spaces = self.indent_spaces
550 571 full_dedent = self._full_dedent
551 572
552 573 inisp = num_ini_spaces(line)
553 574 if inisp < indent_spaces:
554 575 indent_spaces = inisp
555 576 if indent_spaces <= 0:
556 577 #print 'Full dedent in text',self.source # dbg
557 578 full_dedent = True
558 579
559 580 if line[-1] == ':':
560 581 indent_spaces += 4
561 582 elif dedent_re.match(line):
562 583 indent_spaces -= 4
563 584 if indent_spaces <= 0:
564 585 full_dedent = True
565 586
566 587 # Safety
567 588 if indent_spaces < 0:
568 589 indent_spaces = 0
569 590 #print 'safety' # dbg
570 591
571 592 return indent_spaces, full_dedent
572 593
573 594 def _update_indent(self, lines):
574 595 for line in remove_comments(lines).splitlines():
575 596 if line and not line.isspace():
576 597 self.indent_spaces, self._full_dedent = self._find_indent(line)
577 598
578 599 def _store(self, lines):
579 600 """Store one or more lines of input.
580 601
581 602 If input lines are not newline-terminated, a newline is automatically
582 603 appended."""
583 604
584 605 if lines.endswith('\n'):
585 606 self._buffer.append(lines)
586 607 else:
587 608 self._buffer.append(lines+'\n')
588 609 self._set_source()
589 610
590 611 def _set_source(self):
591 612 self.source = ''.join(self._buffer).encode(self.encoding)
592 613
593 614
594 615 #-----------------------------------------------------------------------------
595 616 # Functions and classes for IPython-specific syntactic support
596 617 #-----------------------------------------------------------------------------
597 618
598 619 # RegExp for splitting line contents into pre-char//first word-method//rest.
599 620 # For clarity, each group in on one line.
600 621
601 622 line_split = re.compile("""
602 623 ^(\s*) # any leading space
603 624 ([,;/%]|!!?|\?\??) # escape character or characters
604 625 \s*(%?[\w\.]*) # function/method, possibly with leading %
605 626 # to correctly treat things like '?%magic'
606 627 (\s+.*$|$) # rest of line
607 628 """, re.VERBOSE)
608 629
609 630
610 631 def split_user_input(line):
611 632 """Split user input into early whitespace, esc-char, function part and rest.
612 633
613 634 This is currently handles lines with '=' in them in a very inconsistent
614 635 manner.
615 636
616 637 Examples
617 638 ========
618 639 >>> split_user_input('x=1')
619 640 ('', '', 'x=1', '')
620 641 >>> split_user_input('?')
621 642 ('', '?', '', '')
622 643 >>> split_user_input('??')
623 644 ('', '??', '', '')
624 645 >>> split_user_input(' ?')
625 646 (' ', '?', '', '')
626 647 >>> split_user_input(' ??')
627 648 (' ', '??', '', '')
628 649 >>> split_user_input('??x')
629 650 ('', '??', 'x', '')
630 651 >>> split_user_input('?x=1')
631 652 ('', '', '?x=1', '')
632 653 >>> split_user_input('!ls')
633 654 ('', '!', 'ls', '')
634 655 >>> split_user_input(' !ls')
635 656 (' ', '!', 'ls', '')
636 657 >>> split_user_input('!!ls')
637 658 ('', '!!', 'ls', '')
638 659 >>> split_user_input(' !!ls')
639 660 (' ', '!!', 'ls', '')
640 661 >>> split_user_input(',ls')
641 662 ('', ',', 'ls', '')
642 663 >>> split_user_input(';ls')
643 664 ('', ';', 'ls', '')
644 665 >>> split_user_input(' ;ls')
645 666 (' ', ';', 'ls', '')
646 667 >>> split_user_input('f.g(x)')
647 668 ('', '', 'f.g(x)', '')
648 669 >>> split_user_input('f.g (x)')
649 670 ('', '', 'f.g', '(x)')
650 671 >>> split_user_input('?%hist')
651 672 ('', '?', '%hist', '')
652 673 """
653 674 match = line_split.match(line)
654 675 if match:
655 676 lspace, esc, fpart, rest = match.groups()
656 677 else:
657 678 # print "match failed for line '%s'" % line
658 679 try:
659 680 fpart, rest = line.split(None, 1)
660 681 except ValueError:
661 682 # print "split failed for line '%s'" % line
662 683 fpart, rest = line,''
663 684 lspace = re.match('^(\s*)(.*)', line).groups()[0]
664 685 esc = ''
665 686
666 687 # fpart has to be a valid python identifier, so it better be only pure
667 688 # ascii, no unicode:
668 689 try:
669 690 fpart = fpart.encode('ascii')
670 691 except UnicodeEncodeError:
671 692 lspace = unicode(lspace)
672 693 rest = fpart + u' ' + rest
673 694 fpart = u''
674 695
675 696 #print 'line:<%s>' % line # dbg
676 697 #print 'esc <%s> fpart <%s> rest <%s>' % (esc,fpart.strip(),rest) # dbg
677 698 return lspace, esc, fpart.strip(), rest.lstrip()
678 699
679 700
680 701 # The escaped translators ALL receive a line where their own escape has been
681 702 # stripped. Only '?' is valid at the end of the line, all others can only be
682 703 # placed at the start.
683 704
684 705 class LineInfo(object):
685 706 """A single line of input and associated info.
686 707
687 708 This is a utility class that mostly wraps the output of
688 709 :func:`split_user_input` into a convenient object to be passed around
689 710 during input transformations.
690 711
691 712 Includes the following as properties:
692 713
693 714 line
694 715 The original, raw line
695 716
696 717 lspace
697 718 Any early whitespace before actual text starts.
698 719
699 720 esc
700 721 The initial esc character (or characters, for double-char escapes like
701 722 '??' or '!!').
702 723
703 724 fpart
704 725 The 'function part', which is basically the maximal initial sequence
705 726 of valid python identifiers and the '.' character. This is what is
706 727 checked for alias and magic transformations, used for auto-calling,
707 728 etc.
708 729
709 730 rest
710 731 Everything else on the line.
711 732 """
712 733 def __init__(self, line):
713 734 self.line = line
714 735 self.lspace, self.esc, self.fpart, self.rest = \
715 736 split_user_input(line)
716 737
717 738 def __str__(self):
718 739 return "LineInfo [%s|%s|%s|%s]" % (self.lspace, self.esc,
719 740 self.fpart, self.rest)
720 741
721 742
722 743 # Transformations of the special syntaxes that don't rely on an explicit escape
723 744 # character but instead on patterns on the input line
724 745
725 746 # The core transformations are implemented as standalone functions that can be
726 747 # tested and validated in isolation. Each of these uses a regexp, we
727 748 # pre-compile these and keep them close to each function definition for clarity
728 749
729 750 _assign_system_re = re.compile(r'(?P<lhs>(\s*)([\w\.]+)((\s*,\s*[\w\.]+)*))'
730 751 r'\s*=\s*!\s*(?P<cmd>.*)')
731 752
732 753 def transform_assign_system(line):
733 754 """Handle the `files = !ls` syntax."""
734 755 m = _assign_system_re.match(line)
735 756 if m is not None:
736 757 cmd = m.group('cmd')
737 758 lhs = m.group('lhs')
738 759 expr = make_quoted_expr(cmd)
739 760 new_line = '%s = get_ipython().getoutput(%s)' % (lhs, expr)
740 761 return new_line
741 762 return line
742 763
743 764
744 765 _assign_magic_re = re.compile(r'(?P<lhs>(\s*)([\w\.]+)((\s*,\s*[\w\.]+)*))'
745 766 r'\s*=\s*%\s*(?P<cmd>.*)')
746 767
747 768 def transform_assign_magic(line):
748 769 """Handle the `a = %who` syntax."""
749 770 m = _assign_magic_re.match(line)
750 771 if m is not None:
751 772 cmd = m.group('cmd')
752 773 lhs = m.group('lhs')
753 774 expr = make_quoted_expr(cmd)
754 775 new_line = '%s = get_ipython().magic(%s)' % (lhs, expr)
755 776 return new_line
756 777 return line
757 778
758 779
759 780 _classic_prompt_re = re.compile(r'^([ \t]*>>> |^[ \t]*\.\.\. )')
760 781
761 782 def transform_classic_prompt(line):
762 783 """Handle inputs that start with '>>> ' syntax."""
763 784
764 785 if not line or line.isspace():
765 786 return line
766 787 m = _classic_prompt_re.match(line)
767 788 if m:
768 789 return line[len(m.group(0)):]
769 790 else:
770 791 return line
771 792
772 793
773 794 _ipy_prompt_re = re.compile(r'^([ \t]*In \[\d+\]: |^[ \t]*\ \ \ \.\.\.+: )')
774 795
775 796 def transform_ipy_prompt(line):
776 797 """Handle inputs that start classic IPython prompt syntax."""
777 798
778 799 if not line or line.isspace():
779 800 return line
780 801 #print 'LINE: %r' % line # dbg
781 802 m = _ipy_prompt_re.match(line)
782 803 if m:
783 804 #print 'MATCH! %r -> %r' % (line, line[len(m.group(0)):]) # dbg
784 805 return line[len(m.group(0)):]
785 806 else:
786 807 return line
787 808
788 809
789 810 class EscapedTransformer(object):
790 811 """Class to transform lines that are explicitly escaped out."""
791 812
792 813 def __init__(self):
793 814 tr = { ESC_SHELL : self._tr_system,
794 815 ESC_SH_CAP : self._tr_system2,
795 816 ESC_HELP : self._tr_help,
796 817 ESC_HELP2 : self._tr_help,
797 818 ESC_MAGIC : self._tr_magic,
798 819 ESC_QUOTE : self._tr_quote,
799 820 ESC_QUOTE2 : self._tr_quote2,
800 821 ESC_PAREN : self._tr_paren }
801 822 self.tr = tr
802 823
803 824 # Support for syntax transformations that use explicit escapes typed by the
804 825 # user at the beginning of a line
805 826 @staticmethod
806 827 def _tr_system(line_info):
807 828 "Translate lines escaped with: !"
808 829 cmd = line_info.line.lstrip().lstrip(ESC_SHELL)
809 830 return '%sget_ipython().system(%s)' % (line_info.lspace,
810 831 make_quoted_expr(cmd))
811 832
812 833 @staticmethod
813 834 def _tr_system2(line_info):
814 835 "Translate lines escaped with: !!"
815 836 cmd = line_info.line.lstrip()[2:]
816 837 return '%sget_ipython().getoutput(%s)' % (line_info.lspace,
817 838 make_quoted_expr(cmd))
818 839
819 840 @staticmethod
820 841 def _tr_help(line_info):
821 842 "Translate lines escaped with: ?/??"
822 843 # A naked help line should just fire the intro help screen
823 844 if not line_info.line[1:]:
824 845 return 'get_ipython().show_usage()'
825 846
826 847 # There may be one or two '?' at the end, move them to the front so that
827 848 # the rest of the logic can assume escapes are at the start
828 849 line = line_info.line
829 850 if line.endswith('?'):
830 851 line = line[-1] + line[:-1]
831 852 if line.endswith('?'):
832 853 line = line[-1] + line[:-1]
833 854 line_info = LineInfo(line)
834 855
835 856 # From here on, simply choose which level of detail to get.
836 857 if line_info.esc == '?':
837 858 pinfo = 'pinfo'
838 859 elif line_info.esc == '??':
839 860 pinfo = 'pinfo2'
840 861
841 862 tpl = '%sget_ipython().magic("%s %s")'
842 863 return tpl % (line_info.lspace, pinfo,
843 864 ' '.join([line_info.fpart, line_info.rest]).strip())
844 865
845 866 @staticmethod
846 867 def _tr_magic(line_info):
847 868 "Translate lines escaped with: %"
848 869 tpl = '%sget_ipython().magic(%s)'
849 870 cmd = make_quoted_expr(' '.join([line_info.fpart,
850 871 line_info.rest]).strip())
851 872 return tpl % (line_info.lspace, cmd)
852 873
853 874 @staticmethod
854 875 def _tr_quote(line_info):
855 876 "Translate lines escaped with: ,"
856 877 return '%s%s("%s")' % (line_info.lspace, line_info.fpart,
857 878 '", "'.join(line_info.rest.split()) )
858 879
859 880 @staticmethod
860 881 def _tr_quote2(line_info):
861 882 "Translate lines escaped with: ;"
862 883 return '%s%s("%s")' % (line_info.lspace, line_info.fpart,
863 884 line_info.rest)
864 885
865 886 @staticmethod
866 887 def _tr_paren(line_info):
867 888 "Translate lines escaped with: /"
868 889 return '%s%s(%s)' % (line_info.lspace, line_info.fpart,
869 890 ", ".join(line_info.rest.split()))
870 891
871 892 def __call__(self, line):
872 893 """Class to transform lines that are explicitly escaped out.
873 894
874 895 This calls the above _tr_* static methods for the actual line
875 896 translations."""
876 897
877 898 # Empty lines just get returned unmodified
878 899 if not line or line.isspace():
879 900 return line
880 901
881 902 # Get line endpoints, where the escapes can be
882 903 line_info = LineInfo(line)
883 904
884 905 # If the escape is not at the start, only '?' needs to be special-cased.
885 906 # All other escapes are only valid at the start
886 907 if not line_info.esc in self.tr:
887 908 if line.endswith(ESC_HELP):
888 909 return self._tr_help(line_info)
889 910 else:
890 911 # If we don't recognize the escape, don't modify the line
891 912 return line
892 913
893 914 return self.tr[line_info.esc](line_info)
894 915
895 916
896 917 # A function-looking object to be used by the rest of the code. The purpose of
897 918 # the class in this case is to organize related functionality, more than to
898 919 # manage state.
899 920 transform_escaped = EscapedTransformer()
900 921
901 922
902 923 class IPythonInputSplitter(InputSplitter):
903 924 """An input splitter that recognizes all of IPython's special syntax."""
904 925
905 926 def push(self, lines):
906 927 """Push one or more lines of IPython input.
907 928 """
908 929 if not lines:
909 930 return super(IPythonInputSplitter, self).push(lines)
910 931
911 932 lines_list = lines.splitlines()
912 933
913 934 transforms = [transform_escaped, transform_assign_system,
914 935 transform_assign_magic, transform_ipy_prompt,
915 936 transform_classic_prompt]
916 937
917 938 # Transform logic
918 939 #
919 940 # We only apply the line transformers to the input if we have either no
920 941 # input yet, or complete input, or if the last line of the buffer ends
921 942 # with ':' (opening an indented block). This prevents the accidental
922 943 # transformation of escapes inside multiline expressions like
923 944 # triple-quoted strings or parenthesized expressions.
924 945 #
925 946 # The last heuristic, while ugly, ensures that the first line of an
926 947 # indented block is correctly transformed.
927 948 #
928 949 # FIXME: try to find a cleaner approach for this last bit.
929 950
930 951 # If we were in 'block' mode, since we're going to pump the parent
931 952 # class by hand line by line, we need to temporarily switch out to
932 953 # 'line' mode, do a single manual reset and then feed the lines one
933 954 # by one. Note that this only matters if the input has more than one
934 955 # line.
935 956 changed_input_mode = False
936 957
937 if len(lines_list)>1 and self.input_mode == 'block':
958 if len(lines_list)>1 and self.input_mode == 'cell':
938 959 self.reset()
939 960 changed_input_mode = True
940 saved_input_mode = 'block'
961 saved_input_mode = 'cell'
941 962 self.input_mode = 'line'
942 963
943 964 try:
944 965 push = super(IPythonInputSplitter, self).push
945 966 for line in lines_list:
946 967 if self._is_complete or not self._buffer or \
947 968 (self._buffer and self._buffer[-1].rstrip().endswith(':')):
948 969 for f in transforms:
949 970 line = f(line)
950 971
951 972 out = push(line)
952 973 finally:
953 974 if changed_input_mode:
954 975 self.input_mode = saved_input_mode
955 976
956 977 return out
@@ -1,649 +1,649 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Tests for the inputsplitter module.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (C) 2010 The IPython Development Team
6 6 #
7 7 # Distributed under the terms of the BSD License. The full license is in
8 8 # the file COPYING, distributed as part of this software.
9 9 #-----------------------------------------------------------------------------
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14 # stdlib
15 15 import unittest
16 16 import sys
17 17
18 18 # Third party
19 19 import nose.tools as nt
20 20
21 21 # Our own
22 22 from IPython.core import inputsplitter as isp
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Semi-complete examples (also used as tests)
26 26 #-----------------------------------------------------------------------------
27 27
28 28 # Note: at the bottom, there's a slightly more complete version of this that
29 29 # can be useful during development of code here.
30 30
31 31 def mini_interactive_loop(raw_input):
32 32 """Minimal example of the logic of an interactive interpreter loop.
33 33
34 34 This serves as an example, and it is used by the test system with a fake
35 35 raw_input that simulates interactive input."""
36 36
37 37 from IPython.core.inputsplitter import InputSplitter
38 38
39 39 isp = InputSplitter()
40 40 # In practice, this input loop would be wrapped in an outside loop to read
41 41 # input indefinitely, until some exit/quit command was issued. Here we
42 42 # only illustrate the basic inner loop.
43 43 while isp.push_accepts_more():
44 44 indent = ' '*isp.indent_spaces
45 45 prompt = '>>> ' + indent
46 46 line = indent + raw_input(prompt)
47 47 isp.push(line)
48 48
49 49 # Here we just return input so we can use it in a test suite, but a real
50 50 # interpreter would instead send it for execution somewhere.
51 51 src = isp.source_reset()
52 52 #print 'Input source was:\n', src # dbg
53 53 return src
54 54
55 55 #-----------------------------------------------------------------------------
56 56 # Test utilities, just for local use
57 57 #-----------------------------------------------------------------------------
58 58
59 59 def assemble(block):
60 60 """Assemble a block into multi-line sub-blocks."""
61 61 return ['\n'.join(sub_block)+'\n' for sub_block in block]
62 62
63 63
64 64 def pseudo_input(lines):
65 65 """Return a function that acts like raw_input but feeds the input list."""
66 66 ilines = iter(lines)
67 67 def raw_in(prompt):
68 68 try:
69 69 return next(ilines)
70 70 except StopIteration:
71 71 return ''
72 72 return raw_in
73 73
74 74 #-----------------------------------------------------------------------------
75 75 # Tests
76 76 #-----------------------------------------------------------------------------
77 77 def test_spaces():
78 78 tests = [('', 0),
79 79 (' ', 1),
80 80 ('\n', 0),
81 81 (' \n', 1),
82 82 ('x', 0),
83 83 (' x', 1),
84 84 (' x',2),
85 85 (' x',4),
86 86 # Note: tabs are counted as a single whitespace!
87 87 ('\tx', 1),
88 88 ('\t x', 2),
89 89 ]
90 90
91 91 for s, nsp in tests:
92 92 nt.assert_equal(isp.num_ini_spaces(s), nsp)
93 93
94 94
95 95 def test_remove_comments():
96 96 tests = [('text', 'text'),
97 97 ('text # comment', 'text '),
98 98 ('text # comment\n', 'text \n'),
99 99 ('text # comment \n', 'text \n'),
100 100 ('line # c \nline\n','line \nline\n'),
101 101 ('line # c \nline#c2 \nline\nline #c\n\n',
102 102 'line \nline\nline\nline \n\n'),
103 103 ]
104 104
105 105 for inp, out in tests:
106 106 nt.assert_equal(isp.remove_comments(inp), out)
107 107
108 108
109 109 def test_get_input_encoding():
110 110 encoding = isp.get_input_encoding()
111 111 nt.assert_true(isinstance(encoding, basestring))
112 112 # simple-minded check that at least encoding a simple string works with the
113 113 # encoding we got.
114 114 nt.assert_equal('test'.encode(encoding), 'test')
115 115
116 116
117 117 class NoInputEncodingTestCase(unittest.TestCase):
118 118 def setUp(self):
119 119 self.old_stdin = sys.stdin
120 120 class X: pass
121 121 fake_stdin = X()
122 122 sys.stdin = fake_stdin
123 123
124 124 def test(self):
125 125 # Verify that if sys.stdin has no 'encoding' attribute we do the right
126 126 # thing
127 127 enc = isp.get_input_encoding()
128 128 self.assertEqual(enc, 'ascii')
129 129
130 130 def tearDown(self):
131 131 sys.stdin = self.old_stdin
132 132
133 133
134 134 class InputSplitterTestCase(unittest.TestCase):
135 135 def setUp(self):
136 136 self.isp = isp.InputSplitter()
137 137
138 138 def test_reset(self):
139 139 isp = self.isp
140 140 isp.push('x=1')
141 141 isp.reset()
142 142 self.assertEqual(isp._buffer, [])
143 143 self.assertEqual(isp.indent_spaces, 0)
144 144 self.assertEqual(isp.source, '')
145 145 self.assertEqual(isp.code, None)
146 146 self.assertEqual(isp._is_complete, False)
147 147
148 148 def test_source(self):
149 149 self.isp._store('1')
150 150 self.isp._store('2')
151 151 self.assertEqual(self.isp.source, '1\n2\n')
152 152 self.assertTrue(len(self.isp._buffer)>0)
153 153 self.assertEqual(self.isp.source_reset(), '1\n2\n')
154 154 self.assertEqual(self.isp._buffer, [])
155 155 self.assertEqual(self.isp.source, '')
156 156
157 157 def test_indent(self):
158 158 isp = self.isp # shorthand
159 159 isp.push('x=1')
160 160 self.assertEqual(isp.indent_spaces, 0)
161 161 isp.push('if 1:\n x=1')
162 162 self.assertEqual(isp.indent_spaces, 4)
163 163 isp.push('y=2\n')
164 164 self.assertEqual(isp.indent_spaces, 0)
165 165 isp.push('if 1:')
166 166 self.assertEqual(isp.indent_spaces, 4)
167 167 isp.push(' x=1')
168 168 self.assertEqual(isp.indent_spaces, 4)
169 169 # Blank lines shouldn't change the indent level
170 170 isp.push(' '*2)
171 171 self.assertEqual(isp.indent_spaces, 4)
172 172
173 173 def test_indent2(self):
174 174 isp = self.isp
175 175 # When a multiline statement contains parens or multiline strings, we
176 176 # shouldn't get confused.
177 177 isp.push("if 1:")
178 178 isp.push(" x = (1+\n 2)")
179 179 self.assertEqual(isp.indent_spaces, 4)
180 180
181 181 def test_dedent(self):
182 182 isp = self.isp # shorthand
183 183 isp.push('if 1:')
184 184 self.assertEqual(isp.indent_spaces, 4)
185 185 isp.push(' pass')
186 186 self.assertEqual(isp.indent_spaces, 0)
187 187
188 188 def test_push(self):
189 189 isp = self.isp
190 190 self.assertTrue(isp.push('x=1'))
191 191
192 192 def test_push2(self):
193 193 isp = self.isp
194 194 self.assertFalse(isp.push('if 1:'))
195 195 for line in [' x=1', '# a comment', ' y=2']:
196 196 self.assertTrue(isp.push(line))
197 197
198 198 def test_push3(self):
199 199 """Test input with leading whitespace"""
200 200 isp = self.isp
201 201 isp.push(' x=1')
202 202 isp.push(' y=2')
203 203 self.assertEqual(isp.source, 'if 1:\n x=1\n y=2\n')
204 204
205 205 def test_replace_mode(self):
206 206 isp = self.isp
207 isp.input_mode = 'block'
207 isp.input_mode = 'cell'
208 208 isp.push('x=1')
209 209 self.assertEqual(isp.source, 'x=1\n')
210 210 isp.push('x=2')
211 211 self.assertEqual(isp.source, 'x=2\n')
212 212
213 213 def test_push_accepts_more(self):
214 214 isp = self.isp
215 215 isp.push('x=1')
216 216 self.assertFalse(isp.push_accepts_more())
217 217
218 218 def test_push_accepts_more2(self):
219 219 isp = self.isp
220 220 isp.push('if 1:')
221 221 self.assertTrue(isp.push_accepts_more())
222 222 isp.push(' x=1')
223 223 self.assertTrue(isp.push_accepts_more())
224 224 isp.push('')
225 225 self.assertFalse(isp.push_accepts_more())
226 226
227 227 def test_push_accepts_more3(self):
228 228 isp = self.isp
229 229 isp.push("x = (2+\n3)")
230 230 self.assertFalse(isp.push_accepts_more())
231 231
232 232 def test_push_accepts_more4(self):
233 233 isp = self.isp
234 234 # When a multiline statement contains parens or multiline strings, we
235 235 # shouldn't get confused.
236 236 # FIXME: we should be able to better handle de-dents in statements like
237 237 # multiline strings and multiline expressions (continued with \ or
238 238 # parens). Right now we aren't handling the indentation tracking quite
239 239 # correctly with this, though in practice it may not be too much of a
240 240 # problem. We'll need to see.
241 241 isp.push("if 1:")
242 242 isp.push(" x = (2+")
243 243 isp.push(" 3)")
244 244 self.assertTrue(isp.push_accepts_more())
245 245 isp.push(" y = 3")
246 246 self.assertTrue(isp.push_accepts_more())
247 247 isp.push('')
248 248 self.assertFalse(isp.push_accepts_more())
249 249
250 250 def test_syntax_error(self):
251 251 isp = self.isp
252 252 # Syntax errors immediately produce a 'ready' block, so the invalid
253 253 # Python can be sent to the kernel for evaluation with possible ipython
254 254 # special-syntax conversion.
255 255 isp.push('run foo')
256 256 self.assertFalse(isp.push_accepts_more())
257 257
258 258 def check_split(self, block_lines, compile=True):
259 259 blocks = assemble(block_lines)
260 260 lines = ''.join(blocks)
261 261 oblock = self.isp.split_blocks(lines)
262 262 self.assertEqual(oblock, blocks)
263 263 if compile:
264 264 for block in blocks:
265 265 self.isp._compile(block)
266 266
267 267 def test_split(self):
268 268 # All blocks of input we want to test in a list. The format for each
269 269 # block is a list of lists, with each inner lists consisting of all the
270 270 # lines (as single-lines) that should make up a sub-block.
271 271
272 272 # Note: do NOT put here sub-blocks that don't compile, as the
273 273 # check_split() routine makes a final verification pass to check that
274 274 # each sub_block, as returned by split_blocks(), does compile
275 275 # correctly.
276 276 all_blocks = [ [['x=1']],
277 277
278 278 [['x=1'],
279 279 ['y=2']],
280 280
281 281 [['x=1',
282 282 '# a comment'],
283 283 ['y=11']],
284 284
285 285 [['if 1:',
286 286 ' x=1'],
287 287 ['y=3']],
288 288
289 289 [['def f(x):',
290 290 ' return x'],
291 291 ['x=1']],
292 292
293 293 [['def f(x):',
294 294 ' x+=1',
295 295 ' ',
296 296 ' return x'],
297 297 ['x=1']],
298 298
299 299 [['def f(x):',
300 300 ' if x>0:',
301 301 ' y=1',
302 302 ' # a comment',
303 303 ' else:',
304 304 ' y=4',
305 305 ' ',
306 306 ' return y'],
307 307 ['x=1'],
308 308 ['if 1:',
309 309 ' y=11'] ],
310 310
311 311 [['for i in range(10):'
312 312 ' x=i**2']],
313 313
314 314 [['for i in range(10):'
315 315 ' x=i**2'],
316 316 ['z = 1']],
317 317 ]
318 318 for block_lines in all_blocks:
319 319 self.check_split(block_lines)
320 320
321 321 def test_split_syntax_errors(self):
322 322 # Block splitting with invalid syntax
323 323 all_blocks = [ [['a syntax error']],
324 324
325 325 [['x=1',
326 326 'another syntax error']],
327 327
328 328 [['for i in range(10):'
329 329 ' yet another error']],
330 330
331 331 ]
332 332 for block_lines in all_blocks:
333 333 self.check_split(block_lines, compile=False)
334 334
335 335
336 336 class InteractiveLoopTestCase(unittest.TestCase):
337 337 """Tests for an interactive loop like a python shell.
338 338 """
339 339 def check_ns(self, lines, ns):
340 340 """Validate that the given input lines produce the resulting namespace.
341 341
342 342 Note: the input lines are given exactly as they would be typed in an
343 343 auto-indenting environment, as mini_interactive_loop above already does
344 344 auto-indenting and prepends spaces to the input.
345 345 """
346 346 src = mini_interactive_loop(pseudo_input(lines))
347 347 test_ns = {}
348 348 exec src in test_ns
349 349 # We can't check that the provided ns is identical to the test_ns,
350 350 # because Python fills test_ns with extra keys (copyright, etc). But
351 351 # we can check that the given dict is *contained* in test_ns
352 352 for k,v in ns.items():
353 353 self.assertEqual(test_ns[k], v)
354 354
355 355 def test_simple(self):
356 356 self.check_ns(['x=1'], dict(x=1))
357 357
358 358 def test_simple2(self):
359 359 self.check_ns(['if 1:', 'x=2'], dict(x=2))
360 360
361 361 def test_xy(self):
362 362 self.check_ns(['x=1; y=2'], dict(x=1, y=2))
363 363
364 364 def test_abc(self):
365 365 self.check_ns(['if 1:','a=1','b=2','c=3'], dict(a=1, b=2, c=3))
366 366
367 367 def test_multi(self):
368 368 self.check_ns(['x =(1+','1+','2)'], dict(x=4))
369 369
370 370
371 371 def test_LineInfo():
372 372 """Simple test for LineInfo construction and str()"""
373 373 linfo = isp.LineInfo(' %cd /home')
374 374 nt.assert_equals(str(linfo), 'LineInfo [ |%|cd|/home]')
375 375
376 376
377 377 def test_split_user_input():
378 378 """Unicode test - split_user_input already has good doctests"""
379 379 line = u"PΓ©rez Fernando"
380 380 parts = isp.split_user_input(line)
381 381 parts_expected = (u'', u'', u'', line)
382 382 nt.assert_equal(parts, parts_expected)
383 383
384 384
385 385 # Transformer tests
386 386 def transform_checker(tests, func):
387 387 """Utility to loop over test inputs"""
388 388 for inp, tr in tests:
389 389 nt.assert_equals(func(inp), tr)
390 390
391 391 # Data for all the syntax tests in the form of lists of pairs of
392 392 # raw/transformed input. We store it here as a global dict so that we can use
393 393 # it both within single-function tests and also to validate the behavior of the
394 394 # larger objects
395 395
396 396 syntax = \
397 397 dict(assign_system =
398 398 [('a =! ls', 'a = get_ipython().getoutput("ls")'),
399 399 ('b = !ls', 'b = get_ipython().getoutput("ls")'),
400 400 ('x=1', 'x=1'), # normal input is unmodified
401 401 (' ',' '), # blank lines are kept intact
402 402 ],
403 403
404 404 assign_magic =
405 405 [('a =% who', 'a = get_ipython().magic("who")'),
406 406 ('b = %who', 'b = get_ipython().magic("who")'),
407 407 ('x=1', 'x=1'), # normal input is unmodified
408 408 (' ',' '), # blank lines are kept intact
409 409 ],
410 410
411 411 classic_prompt =
412 412 [('>>> x=1', 'x=1'),
413 413 ('x=1', 'x=1'), # normal input is unmodified
414 414 (' ', ' '), # blank lines are kept intact
415 415 ('... ', ''), # continuation prompts
416 416 ],
417 417
418 418 ipy_prompt =
419 419 [('In [1]: x=1', 'x=1'),
420 420 ('x=1', 'x=1'), # normal input is unmodified
421 421 (' ',' '), # blank lines are kept intact
422 422 (' ....: ', ''), # continuation prompts
423 423 ],
424 424
425 425 # Tests for the escape transformer to leave normal code alone
426 426 escaped_noesc =
427 427 [ (' ', ' '),
428 428 ('x=1', 'x=1'),
429 429 ],
430 430
431 431 # System calls
432 432 escaped_shell =
433 433 [ ('!ls', 'get_ipython().system("ls")'),
434 434 # Double-escape shell, this means to capture the output of the
435 435 # subprocess and return it
436 436 ('!!ls', 'get_ipython().getoutput("ls")'),
437 437 ],
438 438
439 439 # Help/object info
440 440 escaped_help =
441 441 [ ('?', 'get_ipython().show_usage()'),
442 442 ('?x1', 'get_ipython().magic("pinfo x1")'),
443 443 ('??x2', 'get_ipython().magic("pinfo2 x2")'),
444 444 ('x3?', 'get_ipython().magic("pinfo x3")'),
445 445 ('x4??', 'get_ipython().magic("pinfo2 x4")'),
446 446 ('%hist?', 'get_ipython().magic("pinfo %hist")'),
447 447 ],
448 448
449 449 # Explicit magic calls
450 450 escaped_magic =
451 451 [ ('%cd', 'get_ipython().magic("cd")'),
452 452 ('%cd /home', 'get_ipython().magic("cd /home")'),
453 453 (' %magic', ' get_ipython().magic("magic")'),
454 454 ],
455 455
456 456 # Quoting with separate arguments
457 457 escaped_quote =
458 458 [ (',f', 'f("")'),
459 459 (',f x', 'f("x")'),
460 460 (' ,f y', ' f("y")'),
461 461 (',f a b', 'f("a", "b")'),
462 462 ],
463 463
464 464 # Quoting with single argument
465 465 escaped_quote2 =
466 466 [ (';f', 'f("")'),
467 467 (';f x', 'f("x")'),
468 468 (' ;f y', ' f("y")'),
469 469 (';f a b', 'f("a b")'),
470 470 ],
471 471
472 472 # Simply apply parens
473 473 escaped_paren =
474 474 [ ('/f', 'f()'),
475 475 ('/f x', 'f(x)'),
476 476 (' /f y', ' f(y)'),
477 477 ('/f a b', 'f(a, b)'),
478 478 ],
479 479
480 480 )
481 481
482 482 # multiline syntax examples. Each of these should be a list of lists, with
483 483 # each entry itself having pairs of raw/transformed input. The union (with
484 484 # '\n'.join() of the transformed inputs is what the splitter should produce
485 485 # when fed the raw lines one at a time via push.
486 486 syntax_ml = \
487 487 dict(classic_prompt =
488 488 [ [('>>> for i in range(10):','for i in range(10):'),
489 489 ('... print i',' print i'),
490 490 ('... ', ''),
491 491 ],
492 492 ],
493 493
494 494 ipy_prompt =
495 495 [ [('In [24]: for i in range(10):','for i in range(10):'),
496 496 (' ....: print i',' print i'),
497 497 (' ....: ', ''),
498 498 ],
499 499 ],
500 500 )
501 501
502 502
503 503 def test_assign_system():
504 504 transform_checker(syntax['assign_system'], isp.transform_assign_system)
505 505
506 506
507 507 def test_assign_magic():
508 508 transform_checker(syntax['assign_magic'], isp.transform_assign_magic)
509 509
510 510
511 511 def test_classic_prompt():
512 512 transform_checker(syntax['classic_prompt'], isp.transform_classic_prompt)
513 513 for example in syntax_ml['classic_prompt']:
514 514 transform_checker(example, isp.transform_classic_prompt)
515 515
516 516
517 517 def test_ipy_prompt():
518 518 transform_checker(syntax['ipy_prompt'], isp.transform_ipy_prompt)
519 519 for example in syntax_ml['ipy_prompt']:
520 520 transform_checker(example, isp.transform_ipy_prompt)
521 521
522 522
523 523 def test_escaped_noesc():
524 524 transform_checker(syntax['escaped_noesc'], isp.transform_escaped)
525 525
526 526
527 527 def test_escaped_shell():
528 528 transform_checker(syntax['escaped_shell'], isp.transform_escaped)
529 529
530 530
531 531 def test_escaped_help():
532 532 transform_checker(syntax['escaped_help'], isp.transform_escaped)
533 533
534 534
535 535 def test_escaped_magic():
536 536 transform_checker(syntax['escaped_magic'], isp.transform_escaped)
537 537
538 538
539 539 def test_escaped_quote():
540 540 transform_checker(syntax['escaped_quote'], isp.transform_escaped)
541 541
542 542
543 543 def test_escaped_quote2():
544 544 transform_checker(syntax['escaped_quote2'], isp.transform_escaped)
545 545
546 546
547 547 def test_escaped_paren():
548 548 transform_checker(syntax['escaped_paren'], isp.transform_escaped)
549 549
550 550
551 551 class IPythonInputTestCase(InputSplitterTestCase):
552 552 """By just creating a new class whose .isp is a different instance, we
553 553 re-run the same test battery on the new input splitter.
554 554
555 555 In addition, this runs the tests over the syntax and syntax_ml dicts that
556 556 were tested by individual functions, as part of the OO interface.
557 557 """
558 558
559 559 def setUp(self):
560 560 self.isp = isp.IPythonInputSplitter(input_mode='line')
561 561
562 562 def test_syntax(self):
563 563 """Call all single-line syntax tests from the main object"""
564 564 isp = self.isp
565 565 for example in syntax.itervalues():
566 566 for raw, out_t in example:
567 567 if raw.startswith(' '):
568 568 continue
569 569
570 570 isp.push(raw)
571 571 out = isp.source_reset().rstrip()
572 572 self.assertEqual(out, out_t)
573 573
574 574 def test_syntax_multiline(self):
575 575 isp = self.isp
576 576 for example in syntax_ml.itervalues():
577 577 out_t_parts = []
578 578 for line_pairs in example:
579 579 for raw, out_t_part in line_pairs:
580 580 isp.push(raw)
581 581 out_t_parts.append(out_t_part)
582 582
583 583 out = isp.source_reset().rstrip()
584 584 out_t = '\n'.join(out_t_parts).rstrip()
585 585 self.assertEqual(out, out_t)
586 586
587 587
588 588 class BlockIPythonInputTestCase(IPythonInputTestCase):
589 589
590 590 # Deactivate tests that don't make sense for the block mode
591 591 test_push3 = test_split = lambda s: None
592 592
593 593 def setUp(self):
594 self.isp = isp.IPythonInputSplitter(input_mode='block')
594 self.isp = isp.IPythonInputSplitter(input_mode='cell')
595 595
596 596 def test_syntax_multiline(self):
597 597 isp = self.isp
598 598 for example in syntax_ml.itervalues():
599 599 raw_parts = []
600 600 out_t_parts = []
601 601 for line_pairs in example:
602 602 for raw, out_t_part in line_pairs:
603 603 raw_parts.append(raw)
604 604 out_t_parts.append(out_t_part)
605 605
606 606 raw = '\n'.join(raw_parts)
607 607 out_t = '\n'.join(out_t_parts)
608 608
609 609 isp.push(raw)
610 610 out = isp.source_reset()
611 611 # Match ignoring trailing whitespace
612 612 self.assertEqual(out.rstrip(), out_t.rstrip())
613 613
614 614
615 615 #-----------------------------------------------------------------------------
616 616 # Main - use as a script, mostly for developer experiments
617 617 #-----------------------------------------------------------------------------
618 618
619 619 if __name__ == '__main__':
620 620 # A simple demo for interactive experimentation. This code will not get
621 621 # picked up by any test suite.
622 622 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
623 623
624 624 # configure here the syntax to use, prompt and whether to autoindent
625 625 #isp, start_prompt = InputSplitter(), '>>> '
626 626 isp, start_prompt = IPythonInputSplitter(), 'In> '
627 627
628 628 autoindent = True
629 629 #autoindent = False
630 630
631 631 try:
632 632 while True:
633 633 prompt = start_prompt
634 634 while isp.push_accepts_more():
635 635 indent = ' '*isp.indent_spaces
636 636 if autoindent:
637 637 line = indent + raw_input(prompt+indent)
638 638 else:
639 639 line = raw_input(prompt)
640 640 isp.push(line)
641 641 prompt = '... '
642 642
643 643 # Here we just return input so we can use it in a test suite, but a
644 644 # real interpreter would instead send it for execution somewhere.
645 645 #src = isp.source; raise EOFError # dbg
646 646 src = isp.source_reset()
647 647 print 'Input source was:\n', src
648 648 except EOFError:
649 649 print 'Bye'
@@ -1,1544 +1,1549 b''
1 1 """A base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 from os.path import commonprefix
9 9 import re
10 10 import sys
11 11 from textwrap import dedent
12 12
13 13 # System library imports
14 14 from PyQt4 import QtCore, QtGui
15 15
16 16 # Local imports
17 17 from IPython.config.configurable import Configurable
18 18 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
19 19 from IPython.utils.traitlets import Bool, Enum, Int
20 20 from ansi_code_processor import QtAnsiCodeProcessor
21 21 from completion_widget import CompletionWidget
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Classes
25 25 #-----------------------------------------------------------------------------
26 26
27 27 class ConsoleWidget(Configurable, QtGui.QWidget):
28 28 """ An abstract base class for console-type widgets. This class has
29 29 functionality for:
30 30
31 31 * Maintaining a prompt and editing region
32 32 * Providing the traditional Unix-style console keyboard shortcuts
33 33 * Performing tab completion
34 34 * Paging text
35 35 * Handling ANSI escape codes
36 36
37 37 ConsoleWidget also provides a number of utility methods that will be
38 38 convenient to implementors of a console-style widget.
39 39 """
40 40 __metaclass__ = MetaQObjectHasTraits
41 41
42 42 # Whether to process ANSI escape codes.
43 43 ansi_codes = Bool(True, config=True)
44 44
45 45 # The maximum number of lines of text before truncation. Specifying a
46 46 # non-positive number disables text truncation (not recommended).
47 47 buffer_size = Int(500, config=True)
48 48
49 49 # Whether to use a list widget or plain text output for tab completion.
50 50 gui_completion = Bool(False, config=True)
51 51
52 52 # The type of underlying text widget to use. Valid values are 'plain', which
53 53 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
54 54 # NOTE: this value can only be specified during initialization.
55 55 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
56 56
57 57 # The type of paging to use. Valid values are:
58 58 # 'inside' : The widget pages like a traditional terminal pager.
59 59 # 'hsplit' : When paging is requested, the widget is split
60 60 # horizontally. The top pane contains the console, and the
61 61 # bottom pane contains the paged text.
62 62 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
63 63 # 'custom' : No action is taken by the widget beyond emitting a
64 64 # 'custom_page_requested(str)' signal.
65 65 # 'none' : The text is written directly to the console.
66 66 # NOTE: this value can only be specified during initialization.
67 67 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
68 68 default_value='inside', config=True)
69 69
70 70 # Whether to override ShortcutEvents for the keybindings defined by this
71 71 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
72 72 # priority (when it has focus) over, e.g., window-level menu shortcuts.
73 73 override_shortcuts = Bool(False)
74 74
75 75 # Signals that indicate ConsoleWidget state.
76 76 copy_available = QtCore.pyqtSignal(bool)
77 77 redo_available = QtCore.pyqtSignal(bool)
78 78 undo_available = QtCore.pyqtSignal(bool)
79 79
80 80 # Signal emitted when paging is needed and the paging style has been
81 81 # specified as 'custom'.
82 82 custom_page_requested = QtCore.pyqtSignal(object)
83 83
84 84 # Protected class variables.
85 85 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
86 86 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
87 87 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
88 88 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
89 89 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
90 90 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
91 91 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
92 92 _shortcuts = set(_ctrl_down_remap.keys() +
93 93 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
94 94 QtCore.Qt.Key_V ])
95 95
96 96 #---------------------------------------------------------------------------
97 97 # 'QObject' interface
98 98 #---------------------------------------------------------------------------
99 99
100 100 def __init__(self, parent=None, **kw):
101 101 """ Create a ConsoleWidget.
102 102
103 103 Parameters:
104 104 -----------
105 105 parent : QWidget, optional [default None]
106 106 The parent for this widget.
107 107 """
108 108 QtGui.QWidget.__init__(self, parent)
109 109 Configurable.__init__(self, **kw)
110 110
111 111 # Create the layout and underlying text widget.
112 112 layout = QtGui.QStackedLayout(self)
113 113 layout.setContentsMargins(0, 0, 0, 0)
114 114 self._control = self._create_control()
115 115 self._page_control = None
116 116 self._splitter = None
117 117 if self.paging in ('hsplit', 'vsplit'):
118 118 self._splitter = QtGui.QSplitter()
119 119 if self.paging == 'hsplit':
120 120 self._splitter.setOrientation(QtCore.Qt.Horizontal)
121 121 else:
122 122 self._splitter.setOrientation(QtCore.Qt.Vertical)
123 123 self._splitter.addWidget(self._control)
124 124 layout.addWidget(self._splitter)
125 125 else:
126 126 layout.addWidget(self._control)
127 127
128 128 # Create the paging widget, if necessary.
129 129 if self.paging in ('inside', 'hsplit', 'vsplit'):
130 130 self._page_control = self._create_page_control()
131 131 if self._splitter:
132 132 self._page_control.hide()
133 133 self._splitter.addWidget(self._page_control)
134 134 else:
135 135 layout.addWidget(self._page_control)
136 136
137 137 # Initialize protected variables. Some variables contain useful state
138 138 # information for subclasses; they should be considered read-only.
139 139 self._ansi_processor = QtAnsiCodeProcessor()
140 140 self._completion_widget = CompletionWidget(self._control)
141 141 self._continuation_prompt = '> '
142 142 self._continuation_prompt_html = None
143 143 self._executing = False
144 144 self._prompt = ''
145 145 self._prompt_html = None
146 146 self._prompt_pos = 0
147 147 self._prompt_sep = ''
148 148 self._reading = False
149 149 self._reading_callback = None
150 150 self._tab_width = 8
151 151 self._text_completing_pos = 0
152 152
153 153 # Set a monospaced font.
154 154 self.reset_font()
155 155
156 156 def eventFilter(self, obj, event):
157 157 """ Reimplemented to ensure a console-like behavior in the underlying
158 158 text widgets.
159 159 """
160 160 etype = event.type()
161 161 if etype == QtCore.QEvent.KeyPress:
162 162
163 163 # Re-map keys for all filtered widgets.
164 164 key = event.key()
165 165 if self._control_key_down(event.modifiers()) and \
166 166 key in self._ctrl_down_remap:
167 167 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
168 168 self._ctrl_down_remap[key],
169 169 QtCore.Qt.NoModifier)
170 170 QtGui.qApp.sendEvent(obj, new_event)
171 171 return True
172 172
173 173 elif obj == self._control:
174 174 return self._event_filter_console_keypress(event)
175 175
176 176 elif obj == self._page_control:
177 177 return self._event_filter_page_keypress(event)
178 178
179 179 # Make middle-click paste safe.
180 180 elif etype == QtCore.QEvent.MouseButtonRelease and \
181 181 event.button() == QtCore.Qt.MidButton and \
182 182 obj == self._control.viewport():
183 183 cursor = self._control.cursorForPosition(event.pos())
184 184 self._control.setTextCursor(cursor)
185 185 self.paste(QtGui.QClipboard.Selection)
186 186 return True
187 187
188 188 # Override shortcuts for all filtered widgets.
189 189 elif etype == QtCore.QEvent.ShortcutOverride and \
190 190 self.override_shortcuts and \
191 191 self._control_key_down(event.modifiers()) and \
192 192 event.key() in self._shortcuts:
193 193 event.accept()
194 194 return False
195 195
196 196 # Prevent text from being moved by drag and drop.
197 197 elif etype in (QtCore.QEvent.DragEnter, QtCore.QEvent.DragLeave,
198 198 QtCore.QEvent.DragMove, QtCore.QEvent.Drop):
199 199 return True
200 200
201 201 return super(ConsoleWidget, self).eventFilter(obj, event)
202 202
203 203 #---------------------------------------------------------------------------
204 204 # 'QWidget' interface
205 205 #---------------------------------------------------------------------------
206 206
207 207 def resizeEvent(self, event):
208 208 """ Adjust the scrollbars manually after a resize event.
209 209 """
210 210 super(ConsoleWidget, self).resizeEvent(event)
211 211 self._adjust_scrollbars()
212 212
213 213 def sizeHint(self):
214 214 """ Reimplemented to suggest a size that is 80 characters wide and
215 215 25 lines high.
216 216 """
217 217 font_metrics = QtGui.QFontMetrics(self.font)
218 218 margin = (self._control.frameWidth() +
219 219 self._control.document().documentMargin()) * 2
220 220 style = self.style()
221 221 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
222 222
223 223 # Note 1: Despite my best efforts to take the various margins into
224 224 # account, the width is still coming out a bit too small, so we include
225 225 # a fudge factor of one character here.
226 226 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
227 227 # to a Qt bug on certain Mac OS systems where it returns 0.
228 228 width = font_metrics.width(' ') * 81 + margin
229 229 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
230 230 if self.paging == 'hsplit':
231 231 width = width * 2 + splitwidth
232 232
233 233 height = font_metrics.height() * 25 + margin
234 234 if self.paging == 'vsplit':
235 235 height = height * 2 + splitwidth
236 236
237 237 return QtCore.QSize(width, height)
238 238
239 239 #---------------------------------------------------------------------------
240 240 # 'ConsoleWidget' public interface
241 241 #---------------------------------------------------------------------------
242 242
243 243 def can_copy(self):
244 244 """ Returns whether text can be copied to the clipboard.
245 245 """
246 246 return self._control.textCursor().hasSelection()
247 247
248 248 def can_cut(self):
249 249 """ Returns whether text can be cut to the clipboard.
250 250 """
251 251 cursor = self._control.textCursor()
252 252 return (cursor.hasSelection() and
253 253 self._in_buffer(cursor.anchor()) and
254 254 self._in_buffer(cursor.position()))
255 255
256 256 def can_paste(self):
257 257 """ Returns whether text can be pasted from the clipboard.
258 258 """
259 259 # Only accept text that can be ASCII encoded.
260 260 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
261 261 text = QtGui.QApplication.clipboard().text()
262 262 if not text.isEmpty():
263 263 try:
264 264 str(text)
265 265 return True
266 266 except UnicodeEncodeError:
267 267 pass
268 268 return False
269 269
270 270 def clear(self, keep_input=True):
271 271 """ Clear the console, then write a new prompt. If 'keep_input' is set,
272 272 restores the old input buffer when the new prompt is written.
273 273 """
274 274 if keep_input:
275 275 input_buffer = self.input_buffer
276 276 self._control.clear()
277 277 self._show_prompt()
278 278 if keep_input:
279 279 self.input_buffer = input_buffer
280 280
281 281 def copy(self):
282 282 """ Copy the currently selected text to the clipboard.
283 283 """
284 284 self._control.copy()
285 285
286 286 def cut(self):
287 287 """ Copy the currently selected text to the clipboard and delete it
288 288 if it's inside the input buffer.
289 289 """
290 290 self.copy()
291 291 if self.can_cut():
292 292 self._control.textCursor().removeSelectedText()
293 293
294 294 def execute(self, source=None, hidden=False, interactive=False):
295 295 """ Executes source or the input buffer, possibly prompting for more
296 296 input.
297 297
298 298 Parameters:
299 299 -----------
300 300 source : str, optional
301 301
302 302 The source to execute. If not specified, the input buffer will be
303 303 used. If specified and 'hidden' is False, the input buffer will be
304 304 replaced with the source before execution.
305 305
306 306 hidden : bool, optional (default False)
307 307
308 308 If set, no output will be shown and the prompt will not be modified.
309 309 In other words, it will be completely invisible to the user that
310 310 an execution has occurred.
311 311
312 312 interactive : bool, optional (default False)
313 313
314 314 Whether the console is to treat the source as having been manually
315 315 entered by the user. The effect of this parameter depends on the
316 316 subclass implementation.
317 317
318 318 Raises:
319 319 -------
320 320 RuntimeError
321 321 If incomplete input is given and 'hidden' is True. In this case,
322 322 it is not possible to prompt for more input.
323 323
324 324 Returns:
325 325 --------
326 326 A boolean indicating whether the source was executed.
327 327 """
328 328 # WARNING: The order in which things happen here is very particular, in
329 329 # large part because our syntax highlighting is fragile. If you change
330 330 # something, test carefully!
331 331
332 332 # Decide what to execute.
333 333 if source is None:
334 334 source = self.input_buffer
335 335 if not hidden:
336 336 # A newline is appended later, but it should be considered part
337 337 # of the input buffer.
338 338 source += '\n'
339 339 elif not hidden:
340 340 self.input_buffer = source
341 341
342 342 # Execute the source or show a continuation prompt if it is incomplete.
343 343 complete = self._is_complete(source, interactive)
344 344 if hidden:
345 345 if complete:
346 346 self._execute(source, hidden)
347 347 else:
348 348 error = 'Incomplete noninteractive input: "%s"'
349 349 raise RuntimeError(error % source)
350 350 else:
351 351 if complete:
352 352 self._append_plain_text('\n')
353 353 self._executing_input_buffer = self.input_buffer
354 354 self._executing = True
355 355 self._prompt_finished()
356 356
357 357 # The maximum block count is only in effect during execution.
358 358 # This ensures that _prompt_pos does not become invalid due to
359 359 # text truncation.
360 360 self._control.document().setMaximumBlockCount(self.buffer_size)
361 361
362 362 # Setting a positive maximum block count will automatically
363 363 # disable the undo/redo history, but just to be safe:
364 364 self._control.setUndoRedoEnabled(False)
365 365
366 # Flush all state from the input splitter so the next round of
367 # reading input starts with a clean buffer.
368 self._input_splitter.reset()
369
370 # Call actual execution
366 371 self._execute(source, hidden)
367 372
368 373 else:
369 374 # Do this inside an edit block so continuation prompts are
370 375 # removed seamlessly via undo/redo.
371 376 cursor = self._get_end_cursor()
372 377 cursor.beginEditBlock()
373 378 cursor.insertText('\n')
374 379 self._insert_continuation_prompt(cursor)
375 380 cursor.endEditBlock()
376 381
377 382 # Do not do this inside the edit block. It works as expected
378 383 # when using a QPlainTextEdit control, but does not have an
379 384 # effect when using a QTextEdit. I believe this is a Qt bug.
380 385 self._control.moveCursor(QtGui.QTextCursor.End)
381 386
382 387 return complete
383 388
384 389 def _get_input_buffer(self):
385 390 """ The text that the user has entered entered at the current prompt.
386 391 """
387 392 # If we're executing, the input buffer may not even exist anymore due to
388 393 # the limit imposed by 'buffer_size'. Therefore, we store it.
389 394 if self._executing:
390 395 return self._executing_input_buffer
391 396
392 397 cursor = self._get_end_cursor()
393 398 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
394 399 input_buffer = str(cursor.selection().toPlainText())
395 400
396 401 # Strip out continuation prompts.
397 402 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
398 403
399 404 def _set_input_buffer(self, string):
400 405 """ Replaces the text in the input buffer with 'string'.
401 406 """
402 407 # For now, it is an error to modify the input buffer during execution.
403 408 if self._executing:
404 409 raise RuntimeError("Cannot change input buffer during execution.")
405 410
406 411 # Remove old text.
407 412 cursor = self._get_end_cursor()
408 413 cursor.beginEditBlock()
409 414 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
410 415 cursor.removeSelectedText()
411 416
412 417 # Insert new text with continuation prompts.
413 418 lines = string.splitlines(True)
414 419 if lines:
415 420 self._append_plain_text(lines[0])
416 421 for i in xrange(1, len(lines)):
417 422 if self._continuation_prompt_html is None:
418 423 self._append_plain_text(self._continuation_prompt)
419 424 else:
420 425 self._append_html(self._continuation_prompt_html)
421 426 self._append_plain_text(lines[i])
422 427 cursor.endEditBlock()
423 428 self._control.moveCursor(QtGui.QTextCursor.End)
424 429
425 430 input_buffer = property(_get_input_buffer, _set_input_buffer)
426 431
427 432 def _get_font(self):
428 433 """ The base font being used by the ConsoleWidget.
429 434 """
430 435 return self._control.document().defaultFont()
431 436
432 437 def _set_font(self, font):
433 438 """ Sets the base font for the ConsoleWidget to the specified QFont.
434 439 """
435 440 font_metrics = QtGui.QFontMetrics(font)
436 441 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
437 442
438 443 self._completion_widget.setFont(font)
439 444 self._control.document().setDefaultFont(font)
440 445 if self._page_control:
441 446 self._page_control.document().setDefaultFont(font)
442 447
443 448 font = property(_get_font, _set_font)
444 449
445 450 def paste(self, mode=QtGui.QClipboard.Clipboard):
446 451 """ Paste the contents of the clipboard into the input region.
447 452
448 453 Parameters:
449 454 -----------
450 455 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
451 456
452 457 Controls which part of the system clipboard is used. This can be
453 458 used to access the selection clipboard in X11 and the Find buffer
454 459 in Mac OS. By default, the regular clipboard is used.
455 460 """
456 461 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
457 462 try:
458 463 # Remove any trailing newline, which confuses the GUI and
459 464 # forces the user to backspace.
460 465 text = str(QtGui.QApplication.clipboard().text(mode)).rstrip()
461 466 except UnicodeEncodeError:
462 467 pass
463 468 else:
464 469 self._insert_plain_text_into_buffer(dedent(text))
465 470
466 471 def print_(self, printer):
467 472 """ Print the contents of the ConsoleWidget to the specified QPrinter.
468 473 """
469 474 self._control.print_(printer)
470 475
471 476 def prompt_to_top(self):
472 477 """ Moves the prompt to the top of the viewport.
473 478 """
474 479 if not self._executing:
475 480 scrollbar = self._control.verticalScrollBar()
476 481 scrollbar.setValue(scrollbar.maximum())
477 482 cursor = self._control.textCursor()
478 483 self._control.setTextCursor(self._get_prompt_cursor())
479 484 self._control.ensureCursorVisible()
480 485 QtGui.qApp.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents)
481 486 self._control.setTextCursor(cursor)
482 487
483 488 def redo(self):
484 489 """ Redo the last operation. If there is no operation to redo, nothing
485 490 happens.
486 491 """
487 492 self._control.redo()
488 493
489 494 def reset_font(self):
490 495 """ Sets the font to the default fixed-width font for this platform.
491 496 """
492 497 if sys.platform == 'win32':
493 498 # Consolas ships with Vista/Win7, fallback to Courier if needed
494 499 family, fallback = 'Consolas', 'Courier'
495 500 elif sys.platform == 'darwin':
496 501 # OSX always has Monaco, no need for a fallback
497 502 family, fallback = 'Monaco', None
498 503 else:
499 504 # FIXME: remove Consolas as a default on Linux once our font
500 505 # selections are configurable by the user.
501 506 family, fallback = 'Consolas', 'Monospace'
502 507 font = get_font(family, fallback)
503 508 font.setPointSize(QtGui.qApp.font().pointSize())
504 509 font.setStyleHint(QtGui.QFont.TypeWriter)
505 510 self._set_font(font)
506 511
507 512 def select_all(self):
508 513 """ Selects all the text in the buffer.
509 514 """
510 515 self._control.selectAll()
511 516
512 517 def _get_tab_width(self):
513 518 """ The width (in terms of space characters) for tab characters.
514 519 """
515 520 return self._tab_width
516 521
517 522 def _set_tab_width(self, tab_width):
518 523 """ Sets the width (in terms of space characters) for tab characters.
519 524 """
520 525 font_metrics = QtGui.QFontMetrics(self.font)
521 526 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
522 527
523 528 self._tab_width = tab_width
524 529
525 530 tab_width = property(_get_tab_width, _set_tab_width)
526 531
527 532 def undo(self):
528 533 """ Undo the last operation. If there is no operation to undo, nothing
529 534 happens.
530 535 """
531 536 self._control.undo()
532 537
533 538 #---------------------------------------------------------------------------
534 539 # 'ConsoleWidget' abstract interface
535 540 #---------------------------------------------------------------------------
536 541
537 542 def _is_complete(self, source, interactive):
538 543 """ Returns whether 'source' can be executed. When triggered by an
539 544 Enter/Return key press, 'interactive' is True; otherwise, it is
540 545 False.
541 546 """
542 547 raise NotImplementedError
543 548
544 549 def _execute(self, source, hidden):
545 550 """ Execute 'source'. If 'hidden', do not show any output.
546 551 """
547 552 raise NotImplementedError
548 553
549 554 def _prompt_started_hook(self):
550 555 """ Called immediately after a new prompt is displayed.
551 556 """
552 557 pass
553 558
554 559 def _prompt_finished_hook(self):
555 560 """ Called immediately after a prompt is finished, i.e. when some input
556 561 will be processed and a new prompt displayed.
557 562 """
558 563 pass
559 564
560 565 def _up_pressed(self):
561 566 """ Called when the up key is pressed. Returns whether to continue
562 567 processing the event.
563 568 """
564 569 return True
565 570
566 571 def _down_pressed(self):
567 572 """ Called when the down key is pressed. Returns whether to continue
568 573 processing the event.
569 574 """
570 575 return True
571 576
572 577 def _tab_pressed(self):
573 578 """ Called when the tab key is pressed. Returns whether to continue
574 579 processing the event.
575 580 """
576 581 return False
577 582
578 583 #--------------------------------------------------------------------------
579 584 # 'ConsoleWidget' protected interface
580 585 #--------------------------------------------------------------------------
581 586
582 587 def _append_html(self, html):
583 588 """ Appends html at the end of the console buffer.
584 589 """
585 590 cursor = self._get_end_cursor()
586 591 self._insert_html(cursor, html)
587 592
588 593 def _append_html_fetching_plain_text(self, html):
589 594 """ Appends 'html', then returns the plain text version of it.
590 595 """
591 596 cursor = self._get_end_cursor()
592 597 return self._insert_html_fetching_plain_text(cursor, html)
593 598
594 599 def _append_plain_text(self, text):
595 600 """ Appends plain text at the end of the console buffer, processing
596 601 ANSI codes if enabled.
597 602 """
598 603 cursor = self._get_end_cursor()
599 604 self._insert_plain_text(cursor, text)
600 605
601 606 def _append_plain_text_keeping_prompt(self, text):
602 607 """ Writes 'text' after the current prompt, then restores the old prompt
603 608 with its old input buffer.
604 609 """
605 610 input_buffer = self.input_buffer
606 611 self._append_plain_text('\n')
607 612 self._prompt_finished()
608 613
609 614 self._append_plain_text(text)
610 615 self._show_prompt()
611 616 self.input_buffer = input_buffer
612 617
613 618 def _cancel_text_completion(self):
614 619 """ If text completion is progress, cancel it.
615 620 """
616 621 if self._text_completing_pos:
617 622 self._clear_temporary_buffer()
618 623 self._text_completing_pos = 0
619 624
620 625 def _clear_temporary_buffer(self):
621 626 """ Clears the "temporary text" buffer, i.e. all the text following
622 627 the prompt region.
623 628 """
624 629 # Select and remove all text below the input buffer.
625 630 cursor = self._get_prompt_cursor()
626 631 prompt = self._continuation_prompt.lstrip()
627 632 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
628 633 temp_cursor = QtGui.QTextCursor(cursor)
629 634 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
630 635 text = str(temp_cursor.selection().toPlainText()).lstrip()
631 636 if not text.startswith(prompt):
632 637 break
633 638 else:
634 639 # We've reached the end of the input buffer and no text follows.
635 640 return
636 641 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
637 642 cursor.movePosition(QtGui.QTextCursor.End,
638 643 QtGui.QTextCursor.KeepAnchor)
639 644 cursor.removeSelectedText()
640 645
641 646 # After doing this, we have no choice but to clear the undo/redo
642 647 # history. Otherwise, the text is not "temporary" at all, because it
643 648 # can be recalled with undo/redo. Unfortunately, Qt does not expose
644 649 # fine-grained control to the undo/redo system.
645 650 if self._control.isUndoRedoEnabled():
646 651 self._control.setUndoRedoEnabled(False)
647 652 self._control.setUndoRedoEnabled(True)
648 653
649 654 def _complete_with_items(self, cursor, items):
650 655 """ Performs completion with 'items' at the specified cursor location.
651 656 """
652 657 self._cancel_text_completion()
653 658
654 659 if len(items) == 1:
655 660 cursor.setPosition(self._control.textCursor().position(),
656 661 QtGui.QTextCursor.KeepAnchor)
657 662 cursor.insertText(items[0])
658 663
659 664 elif len(items) > 1:
660 665 current_pos = self._control.textCursor().position()
661 666 prefix = commonprefix(items)
662 667 if prefix:
663 668 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
664 669 cursor.insertText(prefix)
665 670 current_pos = cursor.position()
666 671
667 672 if self.gui_completion:
668 673 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
669 674 self._completion_widget.show_items(cursor, items)
670 675 else:
671 676 cursor.beginEditBlock()
672 677 self._append_plain_text('\n')
673 678 self._page(self._format_as_columns(items))
674 679 cursor.endEditBlock()
675 680
676 681 cursor.setPosition(current_pos)
677 682 self._control.moveCursor(QtGui.QTextCursor.End)
678 683 self._control.setTextCursor(cursor)
679 684 self._text_completing_pos = current_pos
680 685
681 686 def _context_menu_make(self, pos):
682 687 """ Creates a context menu for the given QPoint (in widget coordinates).
683 688 """
684 689 menu = QtGui.QMenu()
685 690
686 691 cut_action = menu.addAction('Cut', self.cut)
687 692 cut_action.setEnabled(self.can_cut())
688 693 cut_action.setShortcut(QtGui.QKeySequence.Cut)
689 694
690 695 copy_action = menu.addAction('Copy', self.copy)
691 696 copy_action.setEnabled(self.can_copy())
692 697 copy_action.setShortcut(QtGui.QKeySequence.Copy)
693 698
694 699 paste_action = menu.addAction('Paste', self.paste)
695 700 paste_action.setEnabled(self.can_paste())
696 701 paste_action.setShortcut(QtGui.QKeySequence.Paste)
697 702
698 703 menu.addSeparator()
699 704 menu.addAction('Select All', self.select_all)
700 705
701 706 return menu
702 707
703 708 def _control_key_down(self, modifiers, include_command=True):
704 709 """ Given a KeyboardModifiers flags object, return whether the Control
705 710 key is down.
706 711
707 712 Parameters:
708 713 -----------
709 714 include_command : bool, optional (default True)
710 715 Whether to treat the Command key as a (mutually exclusive) synonym
711 716 for Control when in Mac OS.
712 717 """
713 718 # Note that on Mac OS, ControlModifier corresponds to the Command key
714 719 # while MetaModifier corresponds to the Control key.
715 720 if sys.platform == 'darwin':
716 721 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
717 722 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
718 723 else:
719 724 return bool(modifiers & QtCore.Qt.ControlModifier)
720 725
721 726 def _create_control(self):
722 727 """ Creates and connects the underlying text widget.
723 728 """
724 729 # Create the underlying control.
725 730 if self.kind == 'plain':
726 731 control = QtGui.QPlainTextEdit()
727 732 elif self.kind == 'rich':
728 733 control = QtGui.QTextEdit()
729 734 control.setAcceptRichText(False)
730 735
731 736 # Install event filters. The filter on the viewport is needed for
732 737 # mouse events and drag events.
733 738 control.installEventFilter(self)
734 739 control.viewport().installEventFilter(self)
735 740
736 741 # Connect signals.
737 742 control.cursorPositionChanged.connect(self._cursor_position_changed)
738 743 control.customContextMenuRequested.connect(
739 744 self._custom_context_menu_requested)
740 745 control.copyAvailable.connect(self.copy_available)
741 746 control.redoAvailable.connect(self.redo_available)
742 747 control.undoAvailable.connect(self.undo_available)
743 748
744 749 # Hijack the document size change signal to prevent Qt from adjusting
745 750 # the viewport's scrollbar. We are relying on an implementation detail
746 751 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
747 752 # this functionality we cannot create a nice terminal interface.
748 753 layout = control.document().documentLayout()
749 754 layout.documentSizeChanged.disconnect()
750 755 layout.documentSizeChanged.connect(self._adjust_scrollbars)
751 756
752 757 # Configure the control.
753 758 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
754 759 control.setReadOnly(True)
755 760 control.setUndoRedoEnabled(False)
756 761 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
757 762 return control
758 763
759 764 def _create_page_control(self):
760 765 """ Creates and connects the underlying paging widget.
761 766 """
762 767 control = QtGui.QPlainTextEdit()
763 768 control.installEventFilter(self)
764 769 control.setReadOnly(True)
765 770 control.setUndoRedoEnabled(False)
766 771 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
767 772 return control
768 773
769 774 def _event_filter_console_keypress(self, event):
770 775 """ Filter key events for the underlying text widget to create a
771 776 console-like interface.
772 777 """
773 778 intercepted = False
774 779 cursor = self._control.textCursor()
775 780 position = cursor.position()
776 781 key = event.key()
777 782 ctrl_down = self._control_key_down(event.modifiers())
778 783 alt_down = event.modifiers() & QtCore.Qt.AltModifier
779 784 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
780 785
781 786 #------ Special sequences ----------------------------------------------
782 787
783 788 if event.matches(QtGui.QKeySequence.Copy):
784 789 self.copy()
785 790 intercepted = True
786 791
787 792 elif event.matches(QtGui.QKeySequence.Cut):
788 793 self.cut()
789 794 intercepted = True
790 795
791 796 elif event.matches(QtGui.QKeySequence.Paste):
792 797 self.paste()
793 798 intercepted = True
794 799
795 800 #------ Special modifier logic -----------------------------------------
796 801
797 802 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
798 803 intercepted = True
799 804
800 805 # Special handling when tab completing in text mode.
801 806 self._cancel_text_completion()
802 807
803 808 if self._in_buffer(position):
804 809 if self._reading:
805 810 self._append_plain_text('\n')
806 811 self._reading = False
807 812 if self._reading_callback:
808 813 self._reading_callback()
809 814
810 815 # If there is only whitespace after the cursor, execute.
811 816 # Otherwise, split the line with a continuation prompt.
812 817 elif not self._executing:
813 818 cursor.movePosition(QtGui.QTextCursor.End,
814 819 QtGui.QTextCursor.KeepAnchor)
815 820 at_end = cursor.selectedText().trimmed().isEmpty()
816 821 if (at_end or shift_down) and not ctrl_down:
817 822 self.execute(interactive = not shift_down)
818 823 else:
819 824 # Do this inside an edit block for clean undo/redo.
820 825 cursor.beginEditBlock()
821 826 cursor.setPosition(position)
822 827 cursor.insertText('\n')
823 828 self._insert_continuation_prompt(cursor)
824 829 cursor.endEditBlock()
825 830
826 831 # Ensure that the whole input buffer is visible.
827 832 # FIXME: This will not be usable if the input buffer is
828 833 # taller than the console widget.
829 834 self._control.moveCursor(QtGui.QTextCursor.End)
830 835 self._control.setTextCursor(cursor)
831 836
832 837 #------ Control/Cmd modifier -------------------------------------------
833 838
834 839 elif ctrl_down:
835 840 if key == QtCore.Qt.Key_G:
836 841 self._keyboard_quit()
837 842 intercepted = True
838 843
839 844 elif key == QtCore.Qt.Key_K:
840 845 if self._in_buffer(position):
841 846 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
842 847 QtGui.QTextCursor.KeepAnchor)
843 848 if not cursor.hasSelection():
844 849 # Line deletion (remove continuation prompt)
845 850 cursor.movePosition(QtGui.QTextCursor.NextBlock,
846 851 QtGui.QTextCursor.KeepAnchor)
847 852 cursor.movePosition(QtGui.QTextCursor.Right,
848 853 QtGui.QTextCursor.KeepAnchor,
849 854 len(self._continuation_prompt))
850 855 cursor.removeSelectedText()
851 856 intercepted = True
852 857
853 858 elif key == QtCore.Qt.Key_L:
854 859 self.prompt_to_top()
855 860 intercepted = True
856 861
857 862 elif key == QtCore.Qt.Key_O:
858 863 if self._page_control and self._page_control.isVisible():
859 864 self._page_control.setFocus()
860 865 intercept = True
861 866
862 867 elif key == QtCore.Qt.Key_Y:
863 868 self.paste()
864 869 intercepted = True
865 870
866 871 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
867 872 intercepted = True
868 873
869 874 #------ Alt modifier ---------------------------------------------------
870 875
871 876 elif alt_down:
872 877 if key == QtCore.Qt.Key_B:
873 878 self._set_cursor(self._get_word_start_cursor(position))
874 879 intercepted = True
875 880
876 881 elif key == QtCore.Qt.Key_F:
877 882 self._set_cursor(self._get_word_end_cursor(position))
878 883 intercepted = True
879 884
880 885 elif key == QtCore.Qt.Key_Backspace:
881 886 cursor = self._get_word_start_cursor(position)
882 887 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
883 888 cursor.removeSelectedText()
884 889 intercepted = True
885 890
886 891 elif key == QtCore.Qt.Key_D:
887 892 cursor = self._get_word_end_cursor(position)
888 893 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
889 894 cursor.removeSelectedText()
890 895 intercepted = True
891 896
892 897 elif key == QtCore.Qt.Key_Delete:
893 898 intercepted = True
894 899
895 900 elif key == QtCore.Qt.Key_Greater:
896 901 self._control.moveCursor(QtGui.QTextCursor.End)
897 902 intercepted = True
898 903
899 904 elif key == QtCore.Qt.Key_Less:
900 905 self._control.setTextCursor(self._get_prompt_cursor())
901 906 intercepted = True
902 907
903 908 #------ No modifiers ---------------------------------------------------
904 909
905 910 else:
906 911 if key == QtCore.Qt.Key_Escape:
907 912 self._keyboard_quit()
908 913 intercepted = True
909 914
910 915 elif key == QtCore.Qt.Key_Up:
911 916 if self._reading or not self._up_pressed():
912 917 intercepted = True
913 918 else:
914 919 prompt_line = self._get_prompt_cursor().blockNumber()
915 920 intercepted = cursor.blockNumber() <= prompt_line
916 921
917 922 elif key == QtCore.Qt.Key_Down:
918 923 if self._reading or not self._down_pressed():
919 924 intercepted = True
920 925 else:
921 926 end_line = self._get_end_cursor().blockNumber()
922 927 intercepted = cursor.blockNumber() == end_line
923 928
924 929 elif key == QtCore.Qt.Key_Tab:
925 930 if not self._reading:
926 931 intercepted = not self._tab_pressed()
927 932
928 933 elif key == QtCore.Qt.Key_Left:
929 934 intercepted = not self._in_buffer(position - 1)
930 935
931 936 elif key == QtCore.Qt.Key_Home:
932 937 start_line = cursor.blockNumber()
933 938 if start_line == self._get_prompt_cursor().blockNumber():
934 939 start_pos = self._prompt_pos
935 940 else:
936 941 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
937 942 QtGui.QTextCursor.KeepAnchor)
938 943 start_pos = cursor.position()
939 944 start_pos += len(self._continuation_prompt)
940 945 cursor.setPosition(position)
941 946 if shift_down and self._in_buffer(position):
942 947 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
943 948 else:
944 949 cursor.setPosition(start_pos)
945 950 self._set_cursor(cursor)
946 951 intercepted = True
947 952
948 953 elif key == QtCore.Qt.Key_Backspace:
949 954
950 955 # Line deletion (remove continuation prompt)
951 956 line, col = cursor.blockNumber(), cursor.columnNumber()
952 957 if not self._reading and \
953 958 col == len(self._continuation_prompt) and \
954 959 line > self._get_prompt_cursor().blockNumber():
955 960 cursor.beginEditBlock()
956 961 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
957 962 QtGui.QTextCursor.KeepAnchor)
958 963 cursor.removeSelectedText()
959 964 cursor.deletePreviousChar()
960 965 cursor.endEditBlock()
961 966 intercepted = True
962 967
963 968 # Regular backwards deletion
964 969 else:
965 970 anchor = cursor.anchor()
966 971 if anchor == position:
967 972 intercepted = not self._in_buffer(position - 1)
968 973 else:
969 974 intercepted = not self._in_buffer(min(anchor, position))
970 975
971 976 elif key == QtCore.Qt.Key_Delete:
972 977
973 978 # Line deletion (remove continuation prompt)
974 979 if not self._reading and self._in_buffer(position) and \
975 980 cursor.atBlockEnd() and not cursor.hasSelection():
976 981 cursor.movePosition(QtGui.QTextCursor.NextBlock,
977 982 QtGui.QTextCursor.KeepAnchor)
978 983 cursor.movePosition(QtGui.QTextCursor.Right,
979 984 QtGui.QTextCursor.KeepAnchor,
980 985 len(self._continuation_prompt))
981 986 cursor.removeSelectedText()
982 987 intercepted = True
983 988
984 989 # Regular forwards deletion:
985 990 else:
986 991 anchor = cursor.anchor()
987 992 intercepted = (not self._in_buffer(anchor) or
988 993 not self._in_buffer(position))
989 994
990 995 # Don't move the cursor if control is down to allow copy-paste using
991 996 # the keyboard in any part of the buffer.
992 997 if not ctrl_down:
993 998 self._keep_cursor_in_buffer()
994 999
995 1000 return intercepted
996 1001
997 1002 def _event_filter_page_keypress(self, event):
998 1003 """ Filter key events for the paging widget to create console-like
999 1004 interface.
1000 1005 """
1001 1006 key = event.key()
1002 1007 ctrl_down = self._control_key_down(event.modifiers())
1003 1008 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1004 1009
1005 1010 if ctrl_down:
1006 1011 if key == QtCore.Qt.Key_O:
1007 1012 self._control.setFocus()
1008 1013 intercept = True
1009 1014
1010 1015 elif alt_down:
1011 1016 if key == QtCore.Qt.Key_Greater:
1012 1017 self._page_control.moveCursor(QtGui.QTextCursor.End)
1013 1018 intercepted = True
1014 1019
1015 1020 elif key == QtCore.Qt.Key_Less:
1016 1021 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1017 1022 intercepted = True
1018 1023
1019 1024 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1020 1025 if self._splitter:
1021 1026 self._page_control.hide()
1022 1027 else:
1023 1028 self.layout().setCurrentWidget(self._control)
1024 1029 return True
1025 1030
1026 1031 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1027 1032 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1028 1033 QtCore.Qt.Key_PageDown,
1029 1034 QtCore.Qt.NoModifier)
1030 1035 QtGui.qApp.sendEvent(self._page_control, new_event)
1031 1036 return True
1032 1037
1033 1038 elif key == QtCore.Qt.Key_Backspace:
1034 1039 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1035 1040 QtCore.Qt.Key_PageUp,
1036 1041 QtCore.Qt.NoModifier)
1037 1042 QtGui.qApp.sendEvent(self._page_control, new_event)
1038 1043 return True
1039 1044
1040 1045 return False
1041 1046
1042 1047 def _format_as_columns(self, items, separator=' '):
1043 1048 """ Transform a list of strings into a single string with columns.
1044 1049
1045 1050 Parameters
1046 1051 ----------
1047 1052 items : sequence of strings
1048 1053 The strings to process.
1049 1054
1050 1055 separator : str, optional [default is two spaces]
1051 1056 The string that separates columns.
1052 1057
1053 1058 Returns
1054 1059 -------
1055 1060 The formatted string.
1056 1061 """
1057 1062 # Note: this code is adapted from columnize 0.3.2.
1058 1063 # See http://code.google.com/p/pycolumnize/
1059 1064
1060 1065 # Calculate the number of characters available.
1061 1066 width = self._control.viewport().width()
1062 1067 char_width = QtGui.QFontMetrics(self.font).width(' ')
1063 1068 displaywidth = max(10, (width / char_width) - 1)
1064 1069
1065 1070 # Some degenerate cases.
1066 1071 size = len(items)
1067 1072 if size == 0:
1068 1073 return '\n'
1069 1074 elif size == 1:
1070 1075 return '%s\n' % str(items[0])
1071 1076
1072 1077 # Try every row count from 1 upwards
1073 1078 array_index = lambda nrows, row, col: nrows*col + row
1074 1079 for nrows in range(1, size):
1075 1080 ncols = (size + nrows - 1) // nrows
1076 1081 colwidths = []
1077 1082 totwidth = -len(separator)
1078 1083 for col in range(ncols):
1079 1084 # Get max column width for this column
1080 1085 colwidth = 0
1081 1086 for row in range(nrows):
1082 1087 i = array_index(nrows, row, col)
1083 1088 if i >= size: break
1084 1089 x = items[i]
1085 1090 colwidth = max(colwidth, len(x))
1086 1091 colwidths.append(colwidth)
1087 1092 totwidth += colwidth + len(separator)
1088 1093 if totwidth > displaywidth:
1089 1094 break
1090 1095 if totwidth <= displaywidth:
1091 1096 break
1092 1097
1093 1098 # The smallest number of rows computed and the max widths for each
1094 1099 # column has been obtained. Now we just have to format each of the rows.
1095 1100 string = ''
1096 1101 for row in range(nrows):
1097 1102 texts = []
1098 1103 for col in range(ncols):
1099 1104 i = row + nrows*col
1100 1105 if i >= size:
1101 1106 texts.append('')
1102 1107 else:
1103 1108 texts.append(items[i])
1104 1109 while texts and not texts[-1]:
1105 1110 del texts[-1]
1106 1111 for col in range(len(texts)):
1107 1112 texts[col] = texts[col].ljust(colwidths[col])
1108 1113 string += '%s\n' % str(separator.join(texts))
1109 1114 return string
1110 1115
1111 1116 def _get_block_plain_text(self, block):
1112 1117 """ Given a QTextBlock, return its unformatted text.
1113 1118 """
1114 1119 cursor = QtGui.QTextCursor(block)
1115 1120 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1116 1121 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1117 1122 QtGui.QTextCursor.KeepAnchor)
1118 1123 return str(cursor.selection().toPlainText())
1119 1124
1120 1125 def _get_cursor(self):
1121 1126 """ Convenience method that returns a cursor for the current position.
1122 1127 """
1123 1128 return self._control.textCursor()
1124 1129
1125 1130 def _get_end_cursor(self):
1126 1131 """ Convenience method that returns a cursor for the last character.
1127 1132 """
1128 1133 cursor = self._control.textCursor()
1129 1134 cursor.movePosition(QtGui.QTextCursor.End)
1130 1135 return cursor
1131 1136
1132 1137 def _get_input_buffer_cursor_column(self):
1133 1138 """ Returns the column of the cursor in the input buffer, excluding the
1134 1139 contribution by the prompt, or -1 if there is no such column.
1135 1140 """
1136 1141 prompt = self._get_input_buffer_cursor_prompt()
1137 1142 if prompt is None:
1138 1143 return -1
1139 1144 else:
1140 1145 cursor = self._control.textCursor()
1141 1146 return cursor.columnNumber() - len(prompt)
1142 1147
1143 1148 def _get_input_buffer_cursor_line(self):
1144 1149 """ Returns line of the input buffer that contains the cursor, or None
1145 1150 if there is no such line.
1146 1151 """
1147 1152 prompt = self._get_input_buffer_cursor_prompt()
1148 1153 if prompt is None:
1149 1154 return None
1150 1155 else:
1151 1156 cursor = self._control.textCursor()
1152 1157 text = self._get_block_plain_text(cursor.block())
1153 1158 return text[len(prompt):]
1154 1159
1155 1160 def _get_input_buffer_cursor_prompt(self):
1156 1161 """ Returns the (plain text) prompt for line of the input buffer that
1157 1162 contains the cursor, or None if there is no such line.
1158 1163 """
1159 1164 if self._executing:
1160 1165 return None
1161 1166 cursor = self._control.textCursor()
1162 1167 if cursor.position() >= self._prompt_pos:
1163 1168 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1164 1169 return self._prompt
1165 1170 else:
1166 1171 return self._continuation_prompt
1167 1172 else:
1168 1173 return None
1169 1174
1170 1175 def _get_prompt_cursor(self):
1171 1176 """ Convenience method that returns a cursor for the prompt position.
1172 1177 """
1173 1178 cursor = self._control.textCursor()
1174 1179 cursor.setPosition(self._prompt_pos)
1175 1180 return cursor
1176 1181
1177 1182 def _get_selection_cursor(self, start, end):
1178 1183 """ Convenience method that returns a cursor with text selected between
1179 1184 the positions 'start' and 'end'.
1180 1185 """
1181 1186 cursor = self._control.textCursor()
1182 1187 cursor.setPosition(start)
1183 1188 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1184 1189 return cursor
1185 1190
1186 1191 def _get_word_start_cursor(self, position):
1187 1192 """ Find the start of the word to the left the given position. If a
1188 1193 sequence of non-word characters precedes the first word, skip over
1189 1194 them. (This emulates the behavior of bash, emacs, etc.)
1190 1195 """
1191 1196 document = self._control.document()
1192 1197 position -= 1
1193 1198 while position >= self._prompt_pos and \
1194 1199 not document.characterAt(position).isLetterOrNumber():
1195 1200 position -= 1
1196 1201 while position >= self._prompt_pos and \
1197 1202 document.characterAt(position).isLetterOrNumber():
1198 1203 position -= 1
1199 1204 cursor = self._control.textCursor()
1200 1205 cursor.setPosition(position + 1)
1201 1206 return cursor
1202 1207
1203 1208 def _get_word_end_cursor(self, position):
1204 1209 """ Find the end of the word to the right the given position. If a
1205 1210 sequence of non-word characters precedes the first word, skip over
1206 1211 them. (This emulates the behavior of bash, emacs, etc.)
1207 1212 """
1208 1213 document = self._control.document()
1209 1214 end = self._get_end_cursor().position()
1210 1215 while position < end and \
1211 1216 not document.characterAt(position).isLetterOrNumber():
1212 1217 position += 1
1213 1218 while position < end and \
1214 1219 document.characterAt(position).isLetterOrNumber():
1215 1220 position += 1
1216 1221 cursor = self._control.textCursor()
1217 1222 cursor.setPosition(position)
1218 1223 return cursor
1219 1224
1220 1225 def _insert_continuation_prompt(self, cursor):
1221 1226 """ Inserts new continuation prompt using the specified cursor.
1222 1227 """
1223 1228 if self._continuation_prompt_html is None:
1224 1229 self._insert_plain_text(cursor, self._continuation_prompt)
1225 1230 else:
1226 1231 self._continuation_prompt = self._insert_html_fetching_plain_text(
1227 1232 cursor, self._continuation_prompt_html)
1228 1233
1229 1234 def _insert_html(self, cursor, html):
1230 1235 """ Inserts HTML using the specified cursor in such a way that future
1231 1236 formatting is unaffected.
1232 1237 """
1233 1238 cursor.beginEditBlock()
1234 1239 cursor.insertHtml(html)
1235 1240
1236 1241 # After inserting HTML, the text document "remembers" it's in "html
1237 1242 # mode", which means that subsequent calls adding plain text will result
1238 1243 # in unwanted formatting, lost tab characters, etc. The following code
1239 1244 # hacks around this behavior, which I consider to be a bug in Qt, by
1240 1245 # (crudely) resetting the document's style state.
1241 1246 cursor.movePosition(QtGui.QTextCursor.Left,
1242 1247 QtGui.QTextCursor.KeepAnchor)
1243 1248 if cursor.selection().toPlainText() == ' ':
1244 1249 cursor.removeSelectedText()
1245 1250 else:
1246 1251 cursor.movePosition(QtGui.QTextCursor.Right)
1247 1252 cursor.insertText(' ', QtGui.QTextCharFormat())
1248 1253 cursor.endEditBlock()
1249 1254
1250 1255 def _insert_html_fetching_plain_text(self, cursor, html):
1251 1256 """ Inserts HTML using the specified cursor, then returns its plain text
1252 1257 version.
1253 1258 """
1254 1259 cursor.beginEditBlock()
1255 1260 cursor.removeSelectedText()
1256 1261
1257 1262 start = cursor.position()
1258 1263 self._insert_html(cursor, html)
1259 1264 end = cursor.position()
1260 1265 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1261 1266 text = str(cursor.selection().toPlainText())
1262 1267
1263 1268 cursor.setPosition(end)
1264 1269 cursor.endEditBlock()
1265 1270 return text
1266 1271
1267 1272 def _insert_plain_text(self, cursor, text):
1268 1273 """ Inserts plain text using the specified cursor, processing ANSI codes
1269 1274 if enabled.
1270 1275 """
1271 1276 cursor.beginEditBlock()
1272 1277 if self.ansi_codes:
1273 1278 for substring in self._ansi_processor.split_string(text):
1274 1279 for act in self._ansi_processor.actions:
1275 1280 if ((act.action == 'erase' and act.area == 'screen') or
1276 1281 (act.action == 'scroll' and act.unit == 'page')):
1277 1282 cursor.select(QtGui.QTextCursor.Document)
1278 1283 cursor.removeSelectedText()
1279 1284 format = self._ansi_processor.get_format()
1280 1285 cursor.insertText(substring, format)
1281 1286 else:
1282 1287 cursor.insertText(text)
1283 1288 cursor.endEditBlock()
1284 1289
1285 1290 def _insert_plain_text_into_buffer(self, text):
1286 1291 """ Inserts text into the input buffer at the current cursor position,
1287 1292 ensuring that continuation prompts are inserted as necessary.
1288 1293 """
1289 1294 lines = str(text).splitlines(True)
1290 1295 if lines:
1291 1296 self._keep_cursor_in_buffer()
1292 1297 cursor = self._control.textCursor()
1293 1298 cursor.beginEditBlock()
1294 1299 cursor.insertText(lines[0])
1295 1300 for line in lines[1:]:
1296 1301 if self._continuation_prompt_html is None:
1297 1302 cursor.insertText(self._continuation_prompt)
1298 1303 else:
1299 1304 self._continuation_prompt = \
1300 1305 self._insert_html_fetching_plain_text(
1301 1306 cursor, self._continuation_prompt_html)
1302 1307 cursor.insertText(line)
1303 1308 cursor.endEditBlock()
1304 1309 self._control.setTextCursor(cursor)
1305 1310
1306 1311 def _in_buffer(self, position=None):
1307 1312 """ Returns whether the current cursor (or, if specified, a position) is
1308 1313 inside the editing region.
1309 1314 """
1310 1315 cursor = self._control.textCursor()
1311 1316 if position is None:
1312 1317 position = cursor.position()
1313 1318 else:
1314 1319 cursor.setPosition(position)
1315 1320 line = cursor.blockNumber()
1316 1321 prompt_line = self._get_prompt_cursor().blockNumber()
1317 1322 if line == prompt_line:
1318 1323 return position >= self._prompt_pos
1319 1324 elif line > prompt_line:
1320 1325 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1321 1326 prompt_pos = cursor.position() + len(self._continuation_prompt)
1322 1327 return position >= prompt_pos
1323 1328 return False
1324 1329
1325 1330 def _keep_cursor_in_buffer(self):
1326 1331 """ Ensures that the cursor is inside the editing region. Returns
1327 1332 whether the cursor was moved.
1328 1333 """
1329 1334 moved = not self._in_buffer()
1330 1335 if moved:
1331 1336 cursor = self._control.textCursor()
1332 1337 cursor.movePosition(QtGui.QTextCursor.End)
1333 1338 self._control.setTextCursor(cursor)
1334 1339 return moved
1335 1340
1336 1341 def _keyboard_quit(self):
1337 1342 """ Cancels the current editing task ala Ctrl-G in Emacs.
1338 1343 """
1339 1344 if self._text_completing_pos:
1340 1345 self._cancel_text_completion()
1341 1346 else:
1342 1347 self.input_buffer = ''
1343 1348
1344 1349 def _page(self, text):
1345 1350 """ Displays text using the pager if it exceeds the height of the
1346 1351 visible area.
1347 1352 """
1348 1353 if self.paging == 'none':
1349 1354 self._append_plain_text(text)
1350 1355 else:
1351 1356 line_height = QtGui.QFontMetrics(self.font).height()
1352 1357 minlines = self._control.viewport().height() / line_height
1353 1358 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1354 1359 if self.paging == 'custom':
1355 1360 self.custom_page_requested.emit(text)
1356 1361 else:
1357 1362 self._page_control.clear()
1358 1363 cursor = self._page_control.textCursor()
1359 1364 self._insert_plain_text(cursor, text)
1360 1365 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1361 1366
1362 1367 self._page_control.viewport().resize(self._control.size())
1363 1368 if self._splitter:
1364 1369 self._page_control.show()
1365 1370 self._page_control.setFocus()
1366 1371 else:
1367 1372 self.layout().setCurrentWidget(self._page_control)
1368 1373 else:
1369 1374 self._append_plain_text(text)
1370 1375
1371 1376 def _prompt_finished(self):
1372 1377 """ Called immediately after a prompt is finished, i.e. when some input
1373 1378 will be processed and a new prompt displayed.
1374 1379 """
1375 1380 self._control.setReadOnly(True)
1376 1381 self._prompt_finished_hook()
1377 1382
1378 1383 def _prompt_started(self):
1379 1384 """ Called immediately after a new prompt is displayed.
1380 1385 """
1381 1386 # Temporarily disable the maximum block count to permit undo/redo and
1382 1387 # to ensure that the prompt position does not change due to truncation.
1383 1388 # Because setting this property clears the undo/redo history, we only
1384 1389 # set it if we have to.
1385 1390 if self._control.document().maximumBlockCount() > 0:
1386 1391 self._control.document().setMaximumBlockCount(0)
1387 1392 self._control.setUndoRedoEnabled(True)
1388 1393
1389 1394 self._control.setReadOnly(False)
1390 1395 self._control.moveCursor(QtGui.QTextCursor.End)
1391 1396
1392 1397 self._executing = False
1393 1398 self._prompt_started_hook()
1394 1399
1395 1400 def _readline(self, prompt='', callback=None):
1396 1401 """ Reads one line of input from the user.
1397 1402
1398 1403 Parameters
1399 1404 ----------
1400 1405 prompt : str, optional
1401 1406 The prompt to print before reading the line.
1402 1407
1403 1408 callback : callable, optional
1404 1409 A callback to execute with the read line. If not specified, input is
1405 1410 read *synchronously* and this method does not return until it has
1406 1411 been read.
1407 1412
1408 1413 Returns
1409 1414 -------
1410 1415 If a callback is specified, returns nothing. Otherwise, returns the
1411 1416 input string with the trailing newline stripped.
1412 1417 """
1413 1418 if self._reading:
1414 1419 raise RuntimeError('Cannot read a line. Widget is already reading.')
1415 1420
1416 1421 if not callback and not self.isVisible():
1417 1422 # If the user cannot see the widget, this function cannot return.
1418 1423 raise RuntimeError('Cannot synchronously read a line if the widget '
1419 1424 'is not visible!')
1420 1425
1421 1426 self._reading = True
1422 1427 self._show_prompt(prompt, newline=False)
1423 1428
1424 1429 if callback is None:
1425 1430 self._reading_callback = None
1426 1431 while self._reading:
1427 1432 QtCore.QCoreApplication.processEvents()
1428 1433 return self.input_buffer.rstrip('\n')
1429 1434
1430 1435 else:
1431 1436 self._reading_callback = lambda: \
1432 1437 callback(self.input_buffer.rstrip('\n'))
1433 1438
1434 1439 def _set_continuation_prompt(self, prompt, html=False):
1435 1440 """ Sets the continuation prompt.
1436 1441
1437 1442 Parameters
1438 1443 ----------
1439 1444 prompt : str
1440 1445 The prompt to show when more input is needed.
1441 1446
1442 1447 html : bool, optional (default False)
1443 1448 If set, the prompt will be inserted as formatted HTML. Otherwise,
1444 1449 the prompt will be treated as plain text, though ANSI color codes
1445 1450 will be handled.
1446 1451 """
1447 1452 if html:
1448 1453 self._continuation_prompt_html = prompt
1449 1454 else:
1450 1455 self._continuation_prompt = prompt
1451 1456 self._continuation_prompt_html = None
1452 1457
1453 1458 def _set_cursor(self, cursor):
1454 1459 """ Convenience method to set the current cursor.
1455 1460 """
1456 1461 self._control.setTextCursor(cursor)
1457 1462
1458 1463 def _show_prompt(self, prompt=None, html=False, newline=True):
1459 1464 """ Writes a new prompt at the end of the buffer.
1460 1465
1461 1466 Parameters
1462 1467 ----------
1463 1468 prompt : str, optional
1464 1469 The prompt to show. If not specified, the previous prompt is used.
1465 1470
1466 1471 html : bool, optional (default False)
1467 1472 Only relevant when a prompt is specified. If set, the prompt will
1468 1473 be inserted as formatted HTML. Otherwise, the prompt will be treated
1469 1474 as plain text, though ANSI color codes will be handled.
1470 1475
1471 1476 newline : bool, optional (default True)
1472 1477 If set, a new line will be written before showing the prompt if
1473 1478 there is not already a newline at the end of the buffer.
1474 1479 """
1475 1480 # Insert a preliminary newline, if necessary.
1476 1481 if newline:
1477 1482 cursor = self._get_end_cursor()
1478 1483 if cursor.position() > 0:
1479 1484 cursor.movePosition(QtGui.QTextCursor.Left,
1480 1485 QtGui.QTextCursor.KeepAnchor)
1481 1486 if str(cursor.selection().toPlainText()) != '\n':
1482 1487 self._append_plain_text('\n')
1483 1488
1484 1489 # Write the prompt.
1485 1490 self._append_plain_text(self._prompt_sep)
1486 1491 if prompt is None:
1487 1492 if self._prompt_html is None:
1488 1493 self._append_plain_text(self._prompt)
1489 1494 else:
1490 1495 self._append_html(self._prompt_html)
1491 1496 else:
1492 1497 if html:
1493 1498 self._prompt = self._append_html_fetching_plain_text(prompt)
1494 1499 self._prompt_html = prompt
1495 1500 else:
1496 1501 self._append_plain_text(prompt)
1497 1502 self._prompt = prompt
1498 1503 self._prompt_html = None
1499 1504
1500 1505 self._prompt_pos = self._get_end_cursor().position()
1501 1506 self._prompt_started()
1502 1507
1503 1508 #------ Signal handlers ----------------------------------------------------
1504 1509
1505 1510 def _adjust_scrollbars(self):
1506 1511 """ Expands the vertical scrollbar beyond the range set by Qt.
1507 1512 """
1508 1513 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1509 1514 # and qtextedit.cpp.
1510 1515 document = self._control.document()
1511 1516 scrollbar = self._control.verticalScrollBar()
1512 1517 viewport_height = self._control.viewport().height()
1513 1518 if isinstance(self._control, QtGui.QPlainTextEdit):
1514 1519 high = max(0, document.lineCount() - 1)
1515 1520 step = viewport_height / self._control.fontMetrics().lineSpacing()
1516 1521 else:
1517 1522 high = document.size().height()
1518 1523 step = viewport_height
1519 1524 scrollbar.setRange(0, high)
1520 1525 scrollbar.setPageStep(step)
1521 1526
1522 1527 def _cursor_position_changed(self):
1523 1528 """ Clears the temporary buffer based on the cursor position.
1524 1529 """
1525 1530 if self._text_completing_pos:
1526 1531 document = self._control.document()
1527 1532 if self._text_completing_pos < document.characterCount():
1528 1533 cursor = self._control.textCursor()
1529 1534 pos = cursor.position()
1530 1535 text_cursor = self._control.textCursor()
1531 1536 text_cursor.setPosition(self._text_completing_pos)
1532 1537 if pos < self._text_completing_pos or \
1533 1538 cursor.blockNumber() > text_cursor.blockNumber():
1534 1539 self._clear_temporary_buffer()
1535 1540 self._text_completing_pos = 0
1536 1541 else:
1537 1542 self._clear_temporary_buffer()
1538 1543 self._text_completing_pos = 0
1539 1544
1540 1545 def _custom_context_menu_requested(self, pos):
1541 1546 """ Shows a context menu at the given QPoint (in widget coordinates).
1542 1547 """
1543 1548 menu = self._context_menu_make(pos)
1544 1549 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,530 +1,533 b''
1 from __future__ import print_function
2
1 3 # Standard library imports
2 4 from collections import namedtuple
3 5 import signal
4 6 import sys
5 7
6 8 # System library imports
7 9 from pygments.lexers import PythonLexer
8 10 from PyQt4 import QtCore, QtGui
9 11
10 12 # Local imports
11 13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
12 14 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 15 from IPython.utils.io import raw_print
14 16 from IPython.utils.traitlets import Bool
15 17 from bracket_matcher import BracketMatcher
16 18 from call_tip_widget import CallTipWidget
17 19 from completion_lexer import CompletionLexer
18 20 from history_console_widget import HistoryConsoleWidget
19 21 from pygments_highlighter import PygmentsHighlighter
20 22
21 23
22 24 class FrontendHighlighter(PygmentsHighlighter):
23 25 """ A PygmentsHighlighter that can be turned on and off and that ignores
24 26 prompts.
25 27 """
26 28
27 29 def __init__(self, frontend):
28 30 super(FrontendHighlighter, self).__init__(frontend._control.document())
29 31 self._current_offset = 0
30 32 self._frontend = frontend
31 33 self.highlighting_on = False
32 34
33 35 def highlightBlock(self, qstring):
34 36 """ Highlight a block of text. Reimplemented to highlight selectively.
35 37 """
36 38 if not self.highlighting_on:
37 39 return
38 40
39 41 # The input to this function is unicode string that may contain
40 42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
41 43 # the string as plain text so we can compare it.
42 44 current_block = self.currentBlock()
43 45 string = self._frontend._get_block_plain_text(current_block)
44 46
45 47 # Decide whether to check for the regular or continuation prompt.
46 48 if current_block.contains(self._frontend._prompt_pos):
47 49 prompt = self._frontend._prompt
48 50 else:
49 51 prompt = self._frontend._continuation_prompt
50 52
51 53 # Don't highlight the part of the string that contains the prompt.
52 54 if string.startswith(prompt):
53 55 self._current_offset = len(prompt)
54 56 qstring.remove(0, len(prompt))
55 57 else:
56 58 self._current_offset = 0
57 59
58 60 PygmentsHighlighter.highlightBlock(self, qstring)
59 61
60 62 def rehighlightBlock(self, block):
61 63 """ Reimplemented to temporarily enable highlighting if disabled.
62 64 """
63 65 old = self.highlighting_on
64 66 self.highlighting_on = True
65 67 super(FrontendHighlighter, self).rehighlightBlock(block)
66 68 self.highlighting_on = old
67 69
68 70 def setFormat(self, start, count, format):
69 71 """ Reimplemented to highlight selectively.
70 72 """
71 73 start += self._current_offset
72 74 PygmentsHighlighter.setFormat(self, start, count, format)
73 75
74 76
75 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 78 """ A Qt frontend for a generic Python kernel.
77 79 """
78 80
79 81 # An option and corresponding signal for overriding the default kernel
80 82 # interrupt behavior.
81 83 custom_interrupt = Bool(False)
82 84 custom_interrupt_requested = QtCore.pyqtSignal()
83 85
84 86 # An option and corresponding signals for overriding the default kernel
85 87 # restart behavior.
86 88 custom_restart = Bool(False)
87 89 custom_restart_kernel_died = QtCore.pyqtSignal(float)
88 90 custom_restart_requested = QtCore.pyqtSignal()
89 91
90 92 # Emitted when an 'execute_reply' has been received from the kernel and
91 93 # processed by the FrontendWidget.
92 94 executed = QtCore.pyqtSignal(object)
93 95
94 96 # Emitted when an exit request has been received from the kernel.
95 97 exit_requested = QtCore.pyqtSignal()
96 98
97 99 # Protected class variables.
98 100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
99 101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
100 102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
101 103 _input_splitter_class = InputSplitter
102 104
103 105 #---------------------------------------------------------------------------
104 106 # 'object' interface
105 107 #---------------------------------------------------------------------------
106 108
107 109 def __init__(self, *args, **kw):
108 110 super(FrontendWidget, self).__init__(*args, **kw)
109 111
110 112 # FrontendWidget protected variables.
111 113 self._bracket_matcher = BracketMatcher(self._control)
112 114 self._call_tip_widget = CallTipWidget(self._control)
113 115 self._completion_lexer = CompletionLexer(PythonLexer())
114 116 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
115 117 self._hidden = False
116 118 self._highlighter = FrontendHighlighter(self)
117 self._input_splitter = self._input_splitter_class(input_mode='block')
119 self._input_splitter = self._input_splitter_class(input_mode='cell')
118 120 self._kernel_manager = None
119 121 self._possible_kernel_restart = False
120 122 self._request_info = {}
121 123
122 124 # Configure the ConsoleWidget.
123 125 self.tab_width = 4
124 126 self._set_continuation_prompt('... ')
125 127
126 128 # Configure actions.
127 129 action = self._copy_raw_action
128 130 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
129 131 action.setEnabled(False)
130 132 action.setShortcut(QtGui.QKeySequence(key))
131 133 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
132 134 action.triggered.connect(self.copy_raw)
133 135 self.copy_available.connect(action.setEnabled)
134 136 self.addAction(action)
135 137
136 138 # Connect signal handlers.
137 139 document = self._control.document()
138 140 document.contentsChange.connect(self._document_contents_change)
139 141
140 142 #---------------------------------------------------------------------------
141 143 # 'ConsoleWidget' public interface
142 144 #---------------------------------------------------------------------------
143 145
144 146 def copy(self):
145 147 """ Copy the currently selected text to the clipboard, removing prompts.
146 148 """
147 149 text = str(self._control.textCursor().selection().toPlainText())
148 150 if text:
149 151 # Remove prompts.
150 152 lines = map(transform_classic_prompt, text.splitlines())
151 153 text = '\n'.join(lines)
152 154 # Expand tabs so that we respect PEP-8.
153 155 QtGui.QApplication.clipboard().setText(text.expandtabs(4))
154 156
155 157 #---------------------------------------------------------------------------
156 158 # 'ConsoleWidget' abstract interface
157 159 #---------------------------------------------------------------------------
158 160
159 161 def _is_complete(self, source, interactive):
160 162 """ Returns whether 'source' can be completely processed and a new
161 163 prompt created. When triggered by an Enter/Return key press,
162 164 'interactive' is True; otherwise, it is False.
163 165 """
164 166 complete = self._input_splitter.push(source.expandtabs(4))
165 167 if interactive:
166 168 complete = not self._input_splitter.push_accepts_more()
167 169 return complete
168 170
169 171 def _execute(self, source, hidden):
170 172 """ Execute 'source'. If 'hidden', do not show any output.
171 173
172 174 See parent class :meth:`execute` docstring for full details.
173 175 """
174 176 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
175 177 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
176 178 self._hidden = hidden
177 179
178 180 def _prompt_started_hook(self):
179 181 """ Called immediately after a new prompt is displayed.
180 182 """
181 183 if not self._reading:
182 184 self._highlighter.highlighting_on = True
183 185
184 186 def _prompt_finished_hook(self):
185 187 """ Called immediately after a prompt is finished, i.e. when some input
186 188 will be processed and a new prompt displayed.
187 189 """
188 190 if not self._reading:
189 191 self._highlighter.highlighting_on = False
190 192
191 193 def _tab_pressed(self):
192 194 """ Called when the tab key is pressed. Returns whether to continue
193 195 processing the event.
194 196 """
195 197 # Perform tab completion if:
196 198 # 1) The cursor is in the input buffer.
197 199 # 2) There is a non-whitespace character before the cursor.
198 200 text = self._get_input_buffer_cursor_line()
199 201 if text is None:
200 202 return False
201 203 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
202 204 if complete:
203 205 self._complete()
204 206 return not complete
205 207
206 208 #---------------------------------------------------------------------------
207 209 # 'ConsoleWidget' protected interface
208 210 #---------------------------------------------------------------------------
209 211
210 212 def _context_menu_make(self, pos):
211 213 """ Reimplemented to add an action for raw copy.
212 214 """
213 215 menu = super(FrontendWidget, self)._context_menu_make(pos)
214 216 for before_action in menu.actions():
215 217 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
216 218 QtGui.QKeySequence.ExactMatch:
217 219 menu.insertAction(before_action, self._copy_raw_action)
218 220 break
219 221 return menu
220 222
221 223 def _event_filter_console_keypress(self, event):
222 224 """ Reimplemented to allow execution interruption.
223 225 """
224 226 key = event.key()
225 227 if self._control_key_down(event.modifiers(), include_command=False):
226 228 if key == QtCore.Qt.Key_C and self._executing:
227 229 self.interrupt_kernel()
228 230 return True
229 231 elif key == QtCore.Qt.Key_Period:
230 232 message = 'Are you sure you want to restart the kernel?'
231 233 self.restart_kernel(message, instant_death=False)
232 234 return True
233 235 return super(FrontendWidget, self)._event_filter_console_keypress(event)
234 236
235 237 def _insert_continuation_prompt(self, cursor):
236 238 """ Reimplemented for auto-indentation.
237 239 """
238 240 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
241 #print('SPACES:', self._input_splitter.indent_spaces) # dbg
239 242 spaces = self._input_splitter.indent_spaces
240 243 cursor.insertText('\t' * (spaces / self.tab_width))
241 244 cursor.insertText(' ' * (spaces % self.tab_width))
242 245
243 246 #---------------------------------------------------------------------------
244 247 # 'BaseFrontendMixin' abstract interface
245 248 #---------------------------------------------------------------------------
246 249
247 250 def _handle_complete_reply(self, rep):
248 251 """ Handle replies for tab completion.
249 252 """
250 253 cursor = self._get_cursor()
251 254 info = self._request_info.get('complete')
252 255 if info and info.id == rep['parent_header']['msg_id'] and \
253 256 info.pos == cursor.position():
254 257 text = '.'.join(self._get_context())
255 258 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
256 259 self._complete_with_items(cursor, rep['content']['matches'])
257 260
258 261 def _handle_execute_reply(self, msg):
259 262 """ Handles replies for code execution.
260 263 """
261 264 info = self._request_info.get('execute')
262 265 if info and info.id == msg['parent_header']['msg_id'] and \
263 266 info.kind == 'user' and not self._hidden:
264 267 # Make sure that all output from the SUB channel has been processed
265 268 # before writing a new prompt.
266 269 self.kernel_manager.sub_channel.flush()
267 270
268 271 # Reset the ANSI style information to prevent bad text in stdout
269 272 # from messing up our colors. We're not a true terminal so we're
270 273 # allowed to do this.
271 274 if self.ansi_codes:
272 275 self._ansi_processor.reset_sgr()
273 276
274 277 content = msg['content']
275 278 status = content['status']
276 279 if status == 'ok':
277 280 self._process_execute_ok(msg)
278 281 elif status == 'error':
279 282 self._process_execute_error(msg)
280 283 elif status == 'abort':
281 284 self._process_execute_abort(msg)
282 285
283 286 self._show_interpreter_prompt_for_reply(msg)
284 287 self.executed.emit(msg)
285 288
286 289 def _handle_input_request(self, msg):
287 290 """ Handle requests for raw_input.
288 291 """
289 292 if self._hidden:
290 293 raise RuntimeError('Request for raw input during hidden execution.')
291 294
292 295 # Make sure that all output from the SUB channel has been processed
293 296 # before entering readline mode.
294 297 self.kernel_manager.sub_channel.flush()
295 298
296 299 def callback(line):
297 300 self.kernel_manager.rep_channel.input(line)
298 301 self._readline(msg['content']['prompt'], callback=callback)
299 302
300 303 def _handle_kernel_died(self, since_last_heartbeat):
301 304 """ Handle the kernel's death by asking if the user wants to restart.
302 305 """
303 306 message = 'The kernel heartbeat has been inactive for %.2f ' \
304 307 'seconds. Do you want to restart the kernel? You may ' \
305 308 'first want to check the network connection.' % \
306 309 since_last_heartbeat
307 310 if self.custom_restart:
308 311 self.custom_restart_kernel_died.emit(since_last_heartbeat)
309 312 else:
310 313 self.restart_kernel(message, instant_death=True)
311 314
312 315 def _handle_object_info_reply(self, rep):
313 316 """ Handle replies for call tips.
314 317 """
315 318 cursor = self._get_cursor()
316 319 info = self._request_info.get('call_tip')
317 320 if info and info.id == rep['parent_header']['msg_id'] and \
318 321 info.pos == cursor.position():
319 322 doc = rep['content']['docstring']
320 323 if doc:
321 324 self._call_tip_widget.show_docstring(doc)
322 325
323 326 def _handle_pyout(self, msg):
324 327 """ Handle display hook output.
325 328 """
326 329 if not self._hidden and self._is_from_this_session(msg):
327 330 self._append_plain_text(msg['content']['data'] + '\n')
328 331
329 332 def _handle_stream(self, msg):
330 333 """ Handle stdout, stderr, and stdin.
331 334 """
332 335 if not self._hidden and self._is_from_this_session(msg):
333 336 # Most consoles treat tabs as being 8 space characters. Convert tabs
334 337 # to spaces so that output looks as expected regardless of this
335 338 # widget's tab width.
336 339 text = msg['content']['data'].expandtabs(8)
337 340
338 341 self._append_plain_text(text)
339 342 self._control.moveCursor(QtGui.QTextCursor.End)
340 343
341 344 def _started_channels(self):
342 345 """ Called when the KernelManager channels have started listening or
343 346 when the frontend is assigned an already listening KernelManager.
344 347 """
345 348 self._control.clear()
346 349 self._append_plain_text(self._get_banner())
347 350 self._show_interpreter_prompt()
348 351
349 352 def _stopped_channels(self):
350 353 """ Called when the KernelManager channels have stopped listening or
351 354 when a listening KernelManager is removed from the frontend.
352 355 """
353 356 self._executing = self._reading = False
354 357 self._highlighter.highlighting_on = False
355 358
356 359 #---------------------------------------------------------------------------
357 360 # 'FrontendWidget' public interface
358 361 #---------------------------------------------------------------------------
359 362
360 363 def copy_raw(self):
361 364 """ Copy the currently selected text to the clipboard without attempting
362 365 to remove prompts or otherwise alter the text.
363 366 """
364 367 self._control.copy()
365 368
366 369 def execute_file(self, path, hidden=False):
367 370 """ Attempts to execute file with 'path'. If 'hidden', no output is
368 371 shown.
369 372 """
370 373 self.execute('execfile("%s")' % path, hidden=hidden)
371 374
372 375 def interrupt_kernel(self):
373 376 """ Attempts to interrupt the running kernel.
374 377 """
375 378 if self.custom_interrupt:
376 379 self.custom_interrupt_requested.emit()
377 380 elif self.kernel_manager.has_kernel:
378 381 self.kernel_manager.signal_kernel(signal.SIGINT)
379 382 else:
380 383 self._append_plain_text('Kernel process is either remote or '
381 384 'unspecified. Cannot interrupt.\n')
382 385
383 386 def restart_kernel(self, message, instant_death=False):
384 387 """ Attempts to restart the running kernel.
385 388 """
386 389 # FIXME: instant_death should be configurable via a checkbox in the
387 390 # dialog. Right now at least the heartbeat path sets it to True and
388 391 # the manual restart to False. But those should just be the
389 392 # pre-selected states of a checkbox that the user could override if so
390 393 # desired. But I don't know enough Qt to go implementing the checkbox
391 394 # now.
392 395
393 396 # We want to make sure that if this dialog is already happening, that
394 397 # other signals don't trigger it again. This can happen when the
395 398 # kernel_died heartbeat signal is emitted and the user is slow to
396 399 # respond to the dialog.
397 400 if not self._possible_kernel_restart:
398 401 if self.custom_restart:
399 402 self.custom_restart_requested.emit()
400 403 elif self.kernel_manager.has_kernel:
401 404 # Setting this to True will prevent this logic from happening
402 405 # again until the current pass is completed.
403 406 self._possible_kernel_restart = True
404 407 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
405 408 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
406 409 message, buttons)
407 410 if result == QtGui.QMessageBox.Yes:
408 411 try:
409 412 self.kernel_manager.restart_kernel(
410 413 instant_death=instant_death)
411 414 except RuntimeError:
412 415 message = 'Kernel started externally. Cannot restart.\n'
413 416 self._append_plain_text(message)
414 417 else:
415 418 self._stopped_channels()
416 419 self._append_plain_text('Kernel restarting...\n')
417 420 self._show_interpreter_prompt()
418 421 # This might need to be moved to another location?
419 422 self._possible_kernel_restart = False
420 423 else:
421 424 self._append_plain_text('Kernel process is either remote or '
422 425 'unspecified. Cannot restart.\n')
423 426
424 427 #---------------------------------------------------------------------------
425 428 # 'FrontendWidget' protected interface
426 429 #---------------------------------------------------------------------------
427 430
428 431 def _call_tip(self):
429 432 """ Shows a call tip, if appropriate, at the current cursor location.
430 433 """
431 434 # Decide if it makes sense to show a call tip
432 435 cursor = self._get_cursor()
433 436 cursor.movePosition(QtGui.QTextCursor.Left)
434 437 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
435 438 return False
436 439 context = self._get_context(cursor)
437 440 if not context:
438 441 return False
439 442
440 443 # Send the metadata request to the kernel
441 444 name = '.'.join(context)
442 445 msg_id = self.kernel_manager.xreq_channel.object_info(name)
443 446 pos = self._get_cursor().position()
444 447 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
445 448 return True
446 449
447 450 def _complete(self):
448 451 """ Performs completion at the current cursor location.
449 452 """
450 453 context = self._get_context()
451 454 if context:
452 455 # Send the completion request to the kernel
453 456 msg_id = self.kernel_manager.xreq_channel.complete(
454 457 '.'.join(context), # text
455 458 self._get_input_buffer_cursor_line(), # line
456 459 self._get_input_buffer_cursor_column(), # cursor_pos
457 460 self.input_buffer) # block
458 461 pos = self._get_cursor().position()
459 462 info = self._CompletionRequest(msg_id, pos)
460 463 self._request_info['complete'] = info
461 464
462 465 def _get_banner(self):
463 466 """ Gets a banner to display at the beginning of a session.
464 467 """
465 468 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
466 469 '"license" for more information.'
467 470 return banner % (sys.version, sys.platform)
468 471
469 472 def _get_context(self, cursor=None):
470 473 """ Gets the context for the specified cursor (or the current cursor
471 474 if none is specified).
472 475 """
473 476 if cursor is None:
474 477 cursor = self._get_cursor()
475 478 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
476 479 QtGui.QTextCursor.KeepAnchor)
477 480 text = str(cursor.selection().toPlainText())
478 481 return self._completion_lexer.get_context(text)
479 482
480 483 def _process_execute_abort(self, msg):
481 484 """ Process a reply for an aborted execution request.
482 485 """
483 486 self._append_plain_text("ERROR: execution aborted\n")
484 487
485 488 def _process_execute_error(self, msg):
486 489 """ Process a reply for an execution request that resulted in an error.
487 490 """
488 491 content = msg['content']
489 492 traceback = ''.join(content['traceback'])
490 493 self._append_plain_text(traceback)
491 494
492 495 def _process_execute_ok(self, msg):
493 496 """ Process a reply for a successful execution equest.
494 497 """
495 498 payload = msg['content']['payload']
496 499 for item in payload:
497 500 if not self._process_execute_payload(item):
498 501 warning = 'Warning: received unknown payload of type %s'
499 502 raw_print(warning % repr(item['source']))
500 503
501 504 def _process_execute_payload(self, item):
502 505 """ Process a single payload item from the list of payload items in an
503 506 execution reply. Returns whether the payload was handled.
504 507 """
505 508 # The basic FrontendWidget doesn't handle payloads, as they are a
506 509 # mechanism for going beyond the standard Python interpreter model.
507 510 return False
508 511
509 512 def _show_interpreter_prompt(self):
510 513 """ Shows a prompt for the interpreter.
511 514 """
512 515 self._show_prompt('>>> ')
513 516
514 517 def _show_interpreter_prompt_for_reply(self, msg):
515 518 """ Shows a prompt for the interpreter given an 'execute_reply' message.
516 519 """
517 520 self._show_interpreter_prompt()
518 521
519 522 #------ Signal handlers ----------------------------------------------------
520 523
521 524 def _document_contents_change(self, position, removed, added):
522 525 """ Called whenever the document's content changes. Display a call tip
523 526 if appropriate.
524 527 """
525 528 # Calculate where the cursor should be *after* the change:
526 529 position += added
527 530
528 531 document = self._control.document()
529 532 if position == self._get_cursor().position():
530 533 self._call_tip()
General Comments 0
You need to be logged in to leave comments. Login now