##// END OF EJS Templates
take #!%... prefix into account for completion...
Matthias BUSSONNIER -
Show More
@@ -1,903 +1,907 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 63
64 64 #-----------------------------------------------------------------------------
65 65 # Imports
66 66 #-----------------------------------------------------------------------------
67 67 # stdlib
68 68 import ast
69 69 import codeop
70 70 import re
71 71 import sys
72 72 import tokenize
73 73 from StringIO import StringIO
74 74
75 75 # IPython modules
76 76 from IPython.core.splitinput import split_user_input, LineInfo
77 77 from IPython.utils.py3compat import cast_unicode
78 78
79 79 #-----------------------------------------------------------------------------
80 80 # Globals
81 81 #-----------------------------------------------------------------------------
82 82
83 83 # The escape sequences that define the syntax transformations IPython will
84 84 # apply to user input. These can NOT be just changed here: many regular
85 85 # expressions and other parts of the code may use their hardcoded values, and
86 86 # for all intents and purposes they constitute the 'IPython syntax', so they
87 87 # should be considered fixed.
88 88
89 89 ESC_SHELL = '!' # Send line to underlying system shell
90 90 ESC_SH_CAP = '!!' # Send line to system shell and capture output
91 91 ESC_HELP = '?' # Find information about object
92 92 ESC_HELP2 = '??' # Find extra-detailed information about object
93 93 ESC_MAGIC = '%' # Call magic function
94 94 ESC_MAGIC2 = '%%' # Call cell-magic function
95 95 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
96 96 ESC_QUOTE2 = ';' # Quote all args as a single string, call
97 97 ESC_PAREN = '/' # Call first argument with rest of line as arguments
98 98
99 ESC_SEQUENCES = [ESC_SHELL, ESC_SH_CAP, ESC_HELP ,\
100 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,\
101 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN ]
102
99 103 #-----------------------------------------------------------------------------
100 104 # Utilities
101 105 #-----------------------------------------------------------------------------
102 106
103 107 # FIXME: These are general-purpose utilities that later can be moved to the
104 108 # general ward. Kept here for now because we're being very strict about test
105 109 # coverage with this code, and this lets us ensure that we keep 100% coverage
106 110 # while developing.
107 111
108 112 # compiled regexps for autoindent management
109 113 dedent_re = re.compile('|'.join([
110 114 r'^\s+raise(\s.*)?$', # raise statement (+ space + other stuff, maybe)
111 115 r'^\s+raise\([^\)]*\).*$', # wacky raise with immediate open paren
112 116 r'^\s+return(\s.*)?$', # normal return (+ space + other stuff, maybe)
113 117 r'^\s+return\([^\)]*\).*$', # wacky return with immediate open paren
114 118 r'^\s+pass\s*$' # pass (optionally followed by trailing spaces)
115 119 ]))
116 120 ini_spaces_re = re.compile(r'^([ \t\r\f\v]+)')
117 121
118 122 # regexp to match pure comment lines so we don't accidentally insert 'if 1:'
119 123 # before pure comments
120 124 comment_line_re = re.compile('^\s*\#')
121 125
122 126
123 127 def num_ini_spaces(s):
124 128 """Return the number of initial spaces in a string.
125 129
126 130 Note that tabs are counted as a single space. For now, we do *not* support
127 131 mixing of tabs and spaces in the user's input.
128 132
129 133 Parameters
130 134 ----------
131 135 s : string
132 136
133 137 Returns
134 138 -------
135 139 n : int
136 140 """
137 141
138 142 ini_spaces = ini_spaces_re.match(s)
139 143 if ini_spaces:
140 144 return ini_spaces.end()
141 145 else:
142 146 return 0
143 147
144 148 def last_blank(src):
145 149 """Determine if the input source ends in a blank.
146 150
147 151 A blank is either a newline or a line consisting of whitespace.
148 152
149 153 Parameters
150 154 ----------
151 155 src : string
152 156 A single or multiline string.
153 157 """
154 158 if not src: return False
155 159 ll = src.splitlines()[-1]
156 160 return (ll == '') or ll.isspace()
157 161
158 162
159 163 last_two_blanks_re = re.compile(r'\n\s*\n\s*$', re.MULTILINE)
160 164 last_two_blanks_re2 = re.compile(r'.+\n\s*\n\s+$', re.MULTILINE)
161 165
162 166 def last_two_blanks(src):
163 167 """Determine if the input source ends in two blanks.
164 168
165 169 A blank is either a newline or a line consisting of whitespace.
166 170
167 171 Parameters
168 172 ----------
169 173 src : string
170 174 A single or multiline string.
171 175 """
172 176 if not src: return False
173 177 # The logic here is tricky: I couldn't get a regexp to work and pass all
174 178 # the tests, so I took a different approach: split the source by lines,
175 179 # grab the last two and prepend '###\n' as a stand-in for whatever was in
176 180 # the body before the last two lines. Then, with that structure, it's
177 181 # possible to analyze with two regexps. Not the most elegant solution, but
178 182 # it works. If anyone tries to change this logic, make sure to validate
179 183 # the whole test suite first!
180 184 new_src = '\n'.join(['###\n'] + src.splitlines()[-2:])
181 185 return (bool(last_two_blanks_re.match(new_src)) or
182 186 bool(last_two_blanks_re2.match(new_src)) )
183 187
184 188
185 189 def remove_comments(src):
186 190 """Remove all comments from input source.
187 191
188 192 Note: comments are NOT recognized inside of strings!
189 193
190 194 Parameters
191 195 ----------
192 196 src : string
193 197 A single or multiline input string.
194 198
195 199 Returns
196 200 -------
197 201 String with all Python comments removed.
198 202 """
199 203
200 204 return re.sub('#.*', '', src)
201 205
202 206 def has_comment(src):
203 207 """Indicate whether an input line has (i.e. ends in, or is) a comment.
204 208
205 209 This uses tokenize, so it can distinguish comments from # inside strings.
206 210
207 211 Parameters
208 212 ----------
209 213 src : string
210 214 A single line input string.
211 215
212 216 Returns
213 217 -------
214 218 Boolean: True if source has a comment.
215 219 """
216 220 readline = StringIO(src).readline
217 221 toktypes = set()
218 222 try:
219 223 for t in tokenize.generate_tokens(readline):
220 224 toktypes.add(t[0])
221 225 except tokenize.TokenError:
222 226 pass
223 227 return(tokenize.COMMENT in toktypes)
224 228
225 229
226 230 def get_input_encoding():
227 231 """Return the default standard input encoding.
228 232
229 233 If sys.stdin has no encoding, 'ascii' is returned."""
230 234 # There are strange environments for which sys.stdin.encoding is None. We
231 235 # ensure that a valid encoding is returned.
232 236 encoding = getattr(sys.stdin, 'encoding', None)
233 237 if encoding is None:
234 238 encoding = 'ascii'
235 239 return encoding
236 240
237 241 #-----------------------------------------------------------------------------
238 242 # Classes and functions for normal Python syntax handling
239 243 #-----------------------------------------------------------------------------
240 244
241 245 class InputSplitter(object):
242 246 """An object that can accumulate lines of Python source before execution.
243 247
244 248 This object is designed to be fed python source line-by-line, using
245 249 :meth:`push`. It will return on each push whether the currently pushed
246 250 code could be executed already. In addition, it provides a method called
247 251 :meth:`push_accepts_more` that can be used to query whether more input
248 252 can be pushed into a single interactive block.
249 253
250 254 This is a simple example of how an interactive terminal-based client can use
251 255 this tool::
252 256
253 257 isp = InputSplitter()
254 258 while isp.push_accepts_more():
255 259 indent = ' '*isp.indent_spaces
256 260 prompt = '>>> ' + indent
257 261 line = indent + raw_input(prompt)
258 262 isp.push(line)
259 263 print 'Input source was:\n', isp.source_reset(),
260 264 """
261 265 # Number of spaces of indentation computed from input that has been pushed
262 266 # so far. This is the attributes callers should query to get the current
263 267 # indentation level, in order to provide auto-indent facilities.
264 268 indent_spaces = 0
265 269 # String, indicating the default input encoding. It is computed by default
266 270 # at initialization time via get_input_encoding(), but it can be reset by a
267 271 # client with specific knowledge of the encoding.
268 272 encoding = ''
269 273 # String where the current full source input is stored, properly encoded.
270 274 # Reading this attribute is the normal way of querying the currently pushed
271 275 # source code, that has been properly encoded.
272 276 source = ''
273 277 # Code object corresponding to the current source. It is automatically
274 278 # synced to the source, so it can be queried at any time to obtain the code
275 279 # object; it will be None if the source doesn't compile to valid Python.
276 280 code = None
277 281 # Input mode
278 282 input_mode = 'line'
279 283
280 284 # Private attributes
281 285
282 286 # List with lines of input accumulated so far
283 287 _buffer = None
284 288 # Command compiler
285 289 _compile = None
286 290 # Mark when input has changed indentation all the way back to flush-left
287 291 _full_dedent = False
288 292 # Boolean indicating whether the current block is complete
289 293 _is_complete = None
290 294
291 295 def __init__(self, input_mode=None):
292 296 """Create a new InputSplitter instance.
293 297
294 298 Parameters
295 299 ----------
296 300 input_mode : str
297 301
298 302 One of ['line', 'cell']; default is 'line'.
299 303
300 304 The input_mode parameter controls how new inputs are used when fed via
301 305 the :meth:`push` method:
302 306
303 307 - 'line': meant for line-oriented clients, inputs are appended one at a
304 308 time to the internal buffer and the whole buffer is compiled.
305 309
306 310 - 'cell': meant for clients that can edit multi-line 'cells' of text at
307 311 a time. A cell can contain one or more blocks that can be compile in
308 312 'single' mode by Python. In this mode, each new input new input
309 313 completely replaces all prior inputs. Cell mode is thus equivalent
310 314 to prepending a full reset() to every push() call.
311 315 """
312 316 self._buffer = []
313 317 self._compile = codeop.CommandCompiler()
314 318 self.encoding = get_input_encoding()
315 319 self.input_mode = InputSplitter.input_mode if input_mode is None \
316 320 else input_mode
317 321
318 322 def reset(self):
319 323 """Reset the input buffer and associated state."""
320 324 self.indent_spaces = 0
321 325 self._buffer[:] = []
322 326 self.source = ''
323 327 self.code = None
324 328 self._is_complete = False
325 329 self._full_dedent = False
326 330
327 331 def source_reset(self):
328 332 """Return the input source and perform a full reset.
329 333 """
330 334 out = self.source
331 335 self.reset()
332 336 return out
333 337
334 338 def push(self, lines):
335 339 """Push one or more lines of input.
336 340
337 341 This stores the given lines and returns a status code indicating
338 342 whether the code forms a complete Python block or not.
339 343
340 344 Any exceptions generated in compilation are swallowed, but if an
341 345 exception was produced, the method returns True.
342 346
343 347 Parameters
344 348 ----------
345 349 lines : string
346 350 One or more lines of Python input.
347 351
348 352 Returns
349 353 -------
350 354 is_complete : boolean
351 355 True if the current input source (the result of the current input
352 356 plus prior inputs) forms a complete Python execution block. Note that
353 357 this value is also stored as a private attribute (_is_complete), so it
354 358 can be queried at any time.
355 359 """
356 360 if self.input_mode == 'cell':
357 361 self.reset()
358 362
359 363 self._store(lines)
360 364 source = self.source
361 365
362 366 # Before calling _compile(), reset the code object to None so that if an
363 367 # exception is raised in compilation, we don't mislead by having
364 368 # inconsistent code/source attributes.
365 369 self.code, self._is_complete = None, None
366 370
367 371 # Honor termination lines properly
368 372 if source.rstrip().endswith('\\'):
369 373 return False
370 374
371 375 self._update_indent(lines)
372 376 try:
373 377 self.code = self._compile(source, symbol="exec")
374 378 # Invalid syntax can produce any of a number of different errors from
375 379 # inside the compiler, so we have to catch them all. Syntax errors
376 380 # immediately produce a 'ready' block, so the invalid Python can be
377 381 # sent to the kernel for evaluation with possible ipython
378 382 # special-syntax conversion.
379 383 except (SyntaxError, OverflowError, ValueError, TypeError,
380 384 MemoryError):
381 385 self._is_complete = True
382 386 else:
383 387 # Compilation didn't produce any exceptions (though it may not have
384 388 # given a complete code object)
385 389 self._is_complete = self.code is not None
386 390
387 391 return self._is_complete
388 392
389 393 def push_accepts_more(self):
390 394 """Return whether a block of interactive input can accept more input.
391 395
392 396 This method is meant to be used by line-oriented frontends, who need to
393 397 guess whether a block is complete or not based solely on prior and
394 398 current input lines. The InputSplitter considers it has a complete
395 399 interactive block and will not accept more input only when either a
396 400 SyntaxError is raised, or *all* of the following are true:
397 401
398 402 1. The input compiles to a complete statement.
399 403
400 404 2. The indentation level is flush-left (because if we are indented,
401 405 like inside a function definition or for loop, we need to keep
402 406 reading new input).
403 407
404 408 3. There is one extra line consisting only of whitespace.
405 409
406 410 Because of condition #3, this method should be used only by
407 411 *line-oriented* frontends, since it means that intermediate blank lines
408 412 are not allowed in function definitions (or any other indented block).
409 413
410 414 If the current input produces a syntax error, this method immediately
411 415 returns False but does *not* raise the syntax error exception, as
412 416 typically clients will want to send invalid syntax to an execution
413 417 backend which might convert the invalid syntax into valid Python via
414 418 one of the dynamic IPython mechanisms.
415 419 """
416 420
417 421 # With incomplete input, unconditionally accept more
418 422 if not self._is_complete:
419 423 return True
420 424
421 425 # If we already have complete input and we're flush left, the answer
422 426 # depends. In line mode, if there hasn't been any indentation,
423 427 # that's it. If we've come back from some indentation, we need
424 428 # the blank final line to finish.
425 429 # In cell mode, we need to check how many blocks the input so far
426 430 # compiles into, because if there's already more than one full
427 431 # independent block of input, then the client has entered full
428 432 # 'cell' mode and is feeding lines that each is complete. In this
429 433 # case we should then keep accepting. The Qt terminal-like console
430 434 # does precisely this, to provide the convenience of terminal-like
431 435 # input of single expressions, but allowing the user (with a
432 436 # separate keystroke) to switch to 'cell' mode and type multiple
433 437 # expressions in one shot.
434 438 if self.indent_spaces==0:
435 439 if self.input_mode=='line':
436 440 if not self._full_dedent:
437 441 return False
438 442 else:
439 443 try:
440 444 code_ast = ast.parse(u''.join(self._buffer))
441 445 except Exception:
442 446 return False
443 447 else:
444 448 if len(code_ast.body) == 1:
445 449 return False
446 450
447 451 # When input is complete, then termination is marked by an extra blank
448 452 # line at the end.
449 453 last_line = self.source.splitlines()[-1]
450 454 return bool(last_line and not last_line.isspace())
451 455
452 456 #------------------------------------------------------------------------
453 457 # Private interface
454 458 #------------------------------------------------------------------------
455 459
456 460 def _find_indent(self, line):
457 461 """Compute the new indentation level for a single line.
458 462
459 463 Parameters
460 464 ----------
461 465 line : str
462 466 A single new line of non-whitespace, non-comment Python input.
463 467
464 468 Returns
465 469 -------
466 470 indent_spaces : int
467 471 New value for the indent level (it may be equal to self.indent_spaces
468 472 if indentation doesn't change.
469 473
470 474 full_dedent : boolean
471 475 Whether the new line causes a full flush-left dedent.
472 476 """
473 477 indent_spaces = self.indent_spaces
474 478 full_dedent = self._full_dedent
475 479
476 480 inisp = num_ini_spaces(line)
477 481 if inisp < indent_spaces:
478 482 indent_spaces = inisp
479 483 if indent_spaces <= 0:
480 484 #print 'Full dedent in text',self.source # dbg
481 485 full_dedent = True
482 486
483 487 if line.rstrip()[-1] == ':':
484 488 indent_spaces += 4
485 489 elif dedent_re.match(line):
486 490 indent_spaces -= 4
487 491 if indent_spaces <= 0:
488 492 full_dedent = True
489 493
490 494 # Safety
491 495 if indent_spaces < 0:
492 496 indent_spaces = 0
493 497 #print 'safety' # dbg
494 498
495 499 return indent_spaces, full_dedent
496 500
497 501 def _update_indent(self, lines):
498 502 for line in remove_comments(lines).splitlines():
499 503 if line and not line.isspace():
500 504 self.indent_spaces, self._full_dedent = self._find_indent(line)
501 505
502 506 def _store(self, lines, buffer=None, store='source'):
503 507 """Store one or more lines of input.
504 508
505 509 If input lines are not newline-terminated, a newline is automatically
506 510 appended."""
507 511
508 512 if buffer is None:
509 513 buffer = self._buffer
510 514
511 515 if lines.endswith('\n'):
512 516 buffer.append(lines)
513 517 else:
514 518 buffer.append(lines+'\n')
515 519 setattr(self, store, self._set_source(buffer))
516 520
517 521 def _set_source(self, buffer):
518 522 return u''.join(buffer)
519 523
520 524
521 525 #-----------------------------------------------------------------------------
522 526 # Functions and classes for IPython-specific syntactic support
523 527 #-----------------------------------------------------------------------------
524 528
525 529 # The escaped translators ALL receive a line where their own escape has been
526 530 # stripped. Only '?' is valid at the end of the line, all others can only be
527 531 # placed at the start.
528 532
529 533 # Transformations of the special syntaxes that don't rely on an explicit escape
530 534 # character but instead on patterns on the input line
531 535
532 536 # The core transformations are implemented as standalone functions that can be
533 537 # tested and validated in isolation. Each of these uses a regexp, we
534 538 # pre-compile these and keep them close to each function definition for clarity
535 539
536 540 _assign_system_re = re.compile(r'(?P<lhs>(\s*)([\w\.]+)((\s*,\s*[\w\.]+)*))'
537 541 r'\s*=\s*!\s*(?P<cmd>.*)')
538 542
539 543 def transform_assign_system(line):
540 544 """Handle the `files = !ls` syntax."""
541 545 m = _assign_system_re.match(line)
542 546 if m is not None:
543 547 cmd = m.group('cmd')
544 548 lhs = m.group('lhs')
545 549 new_line = '%s = get_ipython().getoutput(%r)' % (lhs, cmd)
546 550 return new_line
547 551 return line
548 552
549 553
550 554 _assign_magic_re = re.compile(r'(?P<lhs>(\s*)([\w\.]+)((\s*,\s*[\w\.]+)*))'
551 555 r'\s*=\s*%\s*(?P<cmd>.*)')
552 556
553 557 def transform_assign_magic(line):
554 558 """Handle the `a = %who` syntax."""
555 559 m = _assign_magic_re.match(line)
556 560 if m is not None:
557 561 cmd = m.group('cmd')
558 562 lhs = m.group('lhs')
559 563 new_line = '%s = get_ipython().magic(%r)' % (lhs, cmd)
560 564 return new_line
561 565 return line
562 566
563 567
564 568 _classic_prompt_re = re.compile(r'^([ \t]*>>> |^[ \t]*\.\.\. )')
565 569
566 570 def transform_classic_prompt(line):
567 571 """Handle inputs that start with '>>> ' syntax."""
568 572
569 573 if not line or line.isspace():
570 574 return line
571 575 m = _classic_prompt_re.match(line)
572 576 if m:
573 577 return line[len(m.group(0)):]
574 578 else:
575 579 return line
576 580
577 581
578 582 _ipy_prompt_re = re.compile(r'^([ \t]*In \[\d+\]: |^[ \t]*\ \ \ \.\.\.+: )')
579 583
580 584 def transform_ipy_prompt(line):
581 585 """Handle inputs that start classic IPython prompt syntax."""
582 586
583 587 if not line or line.isspace():
584 588 return line
585 589 #print 'LINE: %r' % line # dbg
586 590 m = _ipy_prompt_re.match(line)
587 591 if m:
588 592 #print 'MATCH! %r -> %r' % (line, line[len(m.group(0)):]) # dbg
589 593 return line[len(m.group(0)):]
590 594 else:
591 595 return line
592 596
593 597
594 598 def _make_help_call(target, esc, lspace, next_input=None):
595 599 """Prepares a pinfo(2)/psearch call from a target name and the escape
596 600 (i.e. ? or ??)"""
597 601 method = 'pinfo2' if esc == '??' \
598 602 else 'psearch' if '*' in target \
599 603 else 'pinfo'
600 604 arg = " ".join([method, target])
601 605 if next_input is None:
602 606 return '%sget_ipython().magic(%r)' % (lspace, arg)
603 607 else:
604 608 return '%sget_ipython().set_next_input(%r);get_ipython().magic(%r)' % \
605 609 (lspace, next_input, arg)
606 610
607 611
608 612 _initial_space_re = re.compile(r'\s*')
609 613
610 614 _help_end_re = re.compile(r"""(%{0,2}
611 615 [a-zA-Z_*][\w*]* # Variable name
612 616 (\.[a-zA-Z_*][\w*]*)* # .etc.etc
613 617 )
614 618 (\?\??)$ # ? or ??""",
615 619 re.VERBOSE)
616 620
617 621
618 622 def transform_help_end(line):
619 623 """Translate lines with ?/?? at the end"""
620 624 m = _help_end_re.search(line)
621 625 if m is None or has_comment(line):
622 626 return line
623 627 target = m.group(1)
624 628 esc = m.group(3)
625 629 lspace = _initial_space_re.match(line).group(0)
626 630
627 631 # If we're mid-command, put it back on the next prompt for the user.
628 632 next_input = line.rstrip('?') if line.strip() != m.group(0) else None
629 633
630 634 return _make_help_call(target, esc, lspace, next_input)
631 635
632 636
633 637 class EscapedTransformer(object):
634 638 """Class to transform lines that are explicitly escaped out."""
635 639
636 640 def __init__(self):
637 641 tr = { ESC_SHELL : self._tr_system,
638 642 ESC_SH_CAP : self._tr_system2,
639 643 ESC_HELP : self._tr_help,
640 644 ESC_HELP2 : self._tr_help,
641 645 ESC_MAGIC : self._tr_magic,
642 646 ESC_QUOTE : self._tr_quote,
643 647 ESC_QUOTE2 : self._tr_quote2,
644 648 ESC_PAREN : self._tr_paren }
645 649 self.tr = tr
646 650
647 651 # Support for syntax transformations that use explicit escapes typed by the
648 652 # user at the beginning of a line
649 653 @staticmethod
650 654 def _tr_system(line_info):
651 655 "Translate lines escaped with: !"
652 656 cmd = line_info.line.lstrip().lstrip(ESC_SHELL)
653 657 return '%sget_ipython().system(%r)' % (line_info.pre, cmd)
654 658
655 659 @staticmethod
656 660 def _tr_system2(line_info):
657 661 "Translate lines escaped with: !!"
658 662 cmd = line_info.line.lstrip()[2:]
659 663 return '%sget_ipython().getoutput(%r)' % (line_info.pre, cmd)
660 664
661 665 @staticmethod
662 666 def _tr_help(line_info):
663 667 "Translate lines escaped with: ?/??"
664 668 # A naked help line should just fire the intro help screen
665 669 if not line_info.line[1:]:
666 670 return 'get_ipython().show_usage()'
667 671
668 672 return _make_help_call(line_info.ifun, line_info.esc, line_info.pre)
669 673
670 674 @staticmethod
671 675 def _tr_magic(line_info):
672 676 "Translate lines escaped with: %"
673 677 tpl = '%sget_ipython().magic(%r)'
674 678 cmd = ' '.join([line_info.ifun, line_info.the_rest]).strip()
675 679 return tpl % (line_info.pre, cmd)
676 680
677 681 @staticmethod
678 682 def _tr_quote(line_info):
679 683 "Translate lines escaped with: ,"
680 684 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
681 685 '", "'.join(line_info.the_rest.split()) )
682 686
683 687 @staticmethod
684 688 def _tr_quote2(line_info):
685 689 "Translate lines escaped with: ;"
686 690 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
687 691 line_info.the_rest)
688 692
689 693 @staticmethod
690 694 def _tr_paren(line_info):
691 695 "Translate lines escaped with: /"
692 696 return '%s%s(%s)' % (line_info.pre, line_info.ifun,
693 697 ", ".join(line_info.the_rest.split()))
694 698
695 699 def __call__(self, line):
696 700 """Class to transform lines that are explicitly escaped out.
697 701
698 702 This calls the above _tr_* static methods for the actual line
699 703 translations."""
700 704
701 705 # Empty lines just get returned unmodified
702 706 if not line or line.isspace():
703 707 return line
704 708
705 709 # Get line endpoints, where the escapes can be
706 710 line_info = LineInfo(line)
707 711
708 712 if not line_info.esc in self.tr:
709 713 # If we don't recognize the escape, don't modify the line
710 714 return line
711 715
712 716 return self.tr[line_info.esc](line_info)
713 717
714 718
715 719 # A function-looking object to be used by the rest of the code. The purpose of
716 720 # the class in this case is to organize related functionality, more than to
717 721 # manage state.
718 722 transform_escaped = EscapedTransformer()
719 723
720 724
721 725 class IPythonInputSplitter(InputSplitter):
722 726 """An input splitter that recognizes all of IPython's special syntax."""
723 727
724 728 # String with raw, untransformed input.
725 729 source_raw = ''
726 730
727 731 # Flag to track when we're in the middle of processing a cell magic, since
728 732 # the logic has to change. In that case, we apply no transformations at
729 733 # all.
730 734 processing_cell_magic = False
731 735
732 736 # Storage for all blocks of input that make up a cell magic
733 737 cell_magic_parts = []
734 738
735 739 # Private attributes
736 740
737 741 # List with lines of raw input accumulated so far.
738 742 _buffer_raw = None
739 743
740 744 def __init__(self, input_mode=None):
741 745 super(IPythonInputSplitter, self).__init__(input_mode)
742 746 self._buffer_raw = []
743 747 self._validate = True
744 748
745 749 def reset(self):
746 750 """Reset the input buffer and associated state."""
747 751 super(IPythonInputSplitter, self).reset()
748 752 self._buffer_raw[:] = []
749 753 self.source_raw = ''
750 754 self.cell_magic_parts = []
751 755 self.processing_cell_magic = False
752 756
753 757 def source_raw_reset(self):
754 758 """Return input and raw source and perform a full reset.
755 759 """
756 760 out = self.source
757 761 out_r = self.source_raw
758 762 self.reset()
759 763 return out, out_r
760 764
761 765 def push_accepts_more(self):
762 766 if self.processing_cell_magic:
763 767 return not self._is_complete
764 768 else:
765 769 return super(IPythonInputSplitter, self).push_accepts_more()
766 770
767 771 def _handle_cell_magic(self, lines):
768 772 """Process lines when they start with %%, which marks cell magics.
769 773 """
770 774 self.processing_cell_magic = True
771 775 first, _, body = lines.partition('\n')
772 776 magic_name, _, line = first.partition(' ')
773 777 magic_name = magic_name.lstrip(ESC_MAGIC)
774 778 # We store the body of the cell and create a call to a method that
775 779 # will use this stored value. This is ugly, but it's a first cut to
776 780 # get it all working, as right now changing the return API of our
777 781 # methods would require major refactoring.
778 782 self.cell_magic_parts = [body]
779 783 tpl = 'get_ipython()._run_cached_cell_magic(%r, %r)'
780 784 tlines = tpl % (magic_name, line)
781 785 self._store(tlines)
782 786 self._store(lines, self._buffer_raw, 'source_raw')
783 787 # We can actually choose whether to allow for single blank lines here
784 788 # during input for clients that use cell mode to decide when to stop
785 789 # pushing input (currently only the Qt console).
786 790 # My first implementation did that, and then I realized it wasn't
787 791 # consistent with the terminal behavior, so I've reverted it to one
788 792 # line. But I'm leaving it here so we can easily test both behaviors,
789 793 # I kind of liked having full blank lines allowed in the cell magics...
790 794 #self._is_complete = last_two_blanks(lines)
791 795 self._is_complete = last_blank(lines)
792 796 return self._is_complete
793 797
794 798 def _line_mode_cell_append(self, lines):
795 799 """Append new content for a cell magic in line mode.
796 800 """
797 801 # Only store the raw input. Lines beyond the first one are only only
798 802 # stored for history purposes; for execution the caller will grab the
799 803 # magic pieces from cell_magic_parts and will assemble the cell body
800 804 self._store(lines, self._buffer_raw, 'source_raw')
801 805 self.cell_magic_parts.append(lines)
802 806 # Find out if the last stored block has a whitespace line as its
803 807 # last line and also this line is whitespace, case in which we're
804 808 # done (two contiguous blank lines signal termination). Note that
805 809 # the storage logic *enforces* that every stored block is
806 810 # newline-terminated, so we grab everything but the last character
807 811 # so we can have the body of the block alone.
808 812 last_block = self.cell_magic_parts[-1]
809 813 self._is_complete = last_blank(last_block) and lines.isspace()
810 814 return self._is_complete
811 815
812 816 def push(self, lines):
813 817 """Push one or more lines of IPython input.
814 818
815 819 This stores the given lines and returns a status code indicating
816 820 whether the code forms a complete Python block or not, after processing
817 821 all input lines for special IPython syntax.
818 822
819 823 Any exceptions generated in compilation are swallowed, but if an
820 824 exception was produced, the method returns True.
821 825
822 826 Parameters
823 827 ----------
824 828 lines : string
825 829 One or more lines of Python input.
826 830
827 831 Returns
828 832 -------
829 833 is_complete : boolean
830 834 True if the current input source (the result of the current input
831 835 plus prior inputs) forms a complete Python execution block. Note that
832 836 this value is also stored as a private attribute (_is_complete), so it
833 837 can be queried at any time.
834 838 """
835 839 if not lines:
836 840 return super(IPythonInputSplitter, self).push(lines)
837 841
838 842 # We must ensure all input is pure unicode
839 843 lines = cast_unicode(lines, self.encoding)
840 844
841 845 # If the entire input block is a cell magic, return after handling it
842 846 # as the rest of the transformation logic should be skipped.
843 847 if lines.startswith('%%') and not \
844 848 (len(lines.splitlines()) == 1 and lines.strip().endswith('?')):
845 849 return self._handle_cell_magic(lines)
846 850
847 851 # In line mode, a cell magic can arrive in separate pieces
848 852 if self.input_mode == 'line' and self.processing_cell_magic:
849 853 return self._line_mode_cell_append(lines)
850 854
851 855 # The rest of the processing is for 'normal' content, i.e. IPython
852 856 # source that we process through our transformations pipeline.
853 857 lines_list = lines.splitlines()
854 858
855 859 transforms = [transform_ipy_prompt, transform_classic_prompt,
856 860 transform_help_end, transform_escaped,
857 861 transform_assign_system, transform_assign_magic]
858 862
859 863 # Transform logic
860 864 #
861 865 # We only apply the line transformers to the input if we have either no
862 866 # input yet, or complete input, or if the last line of the buffer ends
863 867 # with ':' (opening an indented block). This prevents the accidental
864 868 # transformation of escapes inside multiline expressions like
865 869 # triple-quoted strings or parenthesized expressions.
866 870 #
867 871 # The last heuristic, while ugly, ensures that the first line of an
868 872 # indented block is correctly transformed.
869 873 #
870 874 # FIXME: try to find a cleaner approach for this last bit.
871 875
872 876 # If we were in 'block' mode, since we're going to pump the parent
873 877 # class by hand line by line, we need to temporarily switch out to
874 878 # 'line' mode, do a single manual reset and then feed the lines one
875 879 # by one. Note that this only matters if the input has more than one
876 880 # line.
877 881 changed_input_mode = False
878 882
879 883 if self.input_mode == 'cell':
880 884 self.reset()
881 885 changed_input_mode = True
882 886 saved_input_mode = 'cell'
883 887 self.input_mode = 'line'
884 888
885 889 # Store raw source before applying any transformations to it. Note
886 890 # that this must be done *after* the reset() call that would otherwise
887 891 # flush the buffer.
888 892 self._store(lines, self._buffer_raw, 'source_raw')
889 893
890 894 try:
891 895 push = super(IPythonInputSplitter, self).push
892 896 buf = self._buffer
893 897 for line in lines_list:
894 898 if self._is_complete or not buf or \
895 899 (buf and buf[-1].rstrip().endswith((':', ','))):
896 900 for f in transforms:
897 901 line = f(line)
898 902
899 903 out = push(line)
900 904 finally:
901 905 if changed_input_mode:
902 906 self.input_mode = saved_input_mode
903 907 return out
@@ -1,1883 +1,1906 b''
1 1 """ An abstract base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 from os.path import commonprefix
8 import os.path
9 9 import re
10 10 import sys
11 11 from textwrap import dedent
12 12 from unicodedata import category
13 13
14 14 # System library imports
15 15 from IPython.external.qt import QtCore, QtGui
16 16
17 17 # Local imports
18 18 from IPython.config.configurable import LoggingConfigurable
19 from IPython.core.inputsplitter import ESC_SEQUENCES
19 20 from IPython.frontend.qt.rich_text import HtmlExporter
20 21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 22 from IPython.utils.text import columnize
22 23 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
23 24 from ansi_code_processor import QtAnsiCodeProcessor
24 25 from completion_widget import CompletionWidget
25 26 from completion_html import CompletionHtml
26 27 from completion_plain import CompletionPlain
27 28 from kill_ring import QtKillRing
28 29
30
29 31 #-----------------------------------------------------------------------------
30 32 # Functions
31 33 #-----------------------------------------------------------------------------
32 34
35 def commonprefix(items):
36 """Given a list of pathnames, returns the longest common leading component
37
38 Same function as os.path.commonprefix, but don't considere prefix made of
39 special caracters like #!$%... see
40
41 IPython.core.inputsplitter import ESC_SEQUENCES
42 """
43 # the last item will always have the least leading % symbol
44 prefixes = ''.join(ESC_SEQUENCES)
45 get_prefix = lambda x : x[0:-len(x.lstrip(prefixes))]
46 # min / max are first/last in alphabetical order
47 first_prefix = get_prefix(min(items))
48 last_prefix = get_prefix(max(items))
49
50 # common suffix is (common prefix of reversed items) reversed
51 prefix = os.path.commonprefix((first_prefix[::-1], last_prefix[::-1]))[::-1]
52
53 items = [ s.lstrip(prefixes) for s in items ]
54 return prefix+os.path.commonprefix(items)
55
33 56 def is_letter_or_number(char):
34 57 """ Returns whether the specified unicode character is a letter or a number.
35 58 """
36 59 cat = category(char)
37 60 return cat.startswith('L') or cat.startswith('N')
38 61
39 62 #-----------------------------------------------------------------------------
40 63 # Classes
41 64 #-----------------------------------------------------------------------------
42 65
43 66 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
44 67 """ An abstract base class for console-type widgets. This class has
45 68 functionality for:
46 69
47 70 * Maintaining a prompt and editing region
48 71 * Providing the traditional Unix-style console keyboard shortcuts
49 72 * Performing tab completion
50 73 * Paging text
51 74 * Handling ANSI escape codes
52 75
53 76 ConsoleWidget also provides a number of utility methods that will be
54 77 convenient to implementors of a console-style widget.
55 78 """
56 79 __metaclass__ = MetaQObjectHasTraits
57 80
58 81 #------ Configuration ------------------------------------------------------
59 82
60 83 ansi_codes = Bool(True, config=True,
61 84 help="Whether to process ANSI escape codes."
62 85 )
63 86 buffer_size = Integer(500, config=True,
64 87 help="""
65 88 The maximum number of lines of text before truncation. Specifying a
66 89 non-positive number disables text truncation (not recommended).
67 90 """
68 91 )
69 92 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
70 93 default_value = 'ncurses',
71 94 help="""
72 95 The type of completer to use. Valid values are:
73 96
74 97 'plain' : Show the availlable completion as a text list
75 98 Below the editting area.
76 99 'droplist': Show the completion in a drop down list navigable
77 100 by the arrow keys, and from which you can select
78 101 completion by pressing Return.
79 102 'ncurses' : Show the completion as a text list which is navigable by
80 103 `tab` and arrow keys.
81 104 """
82 105 )
83 106 # NOTE: this value can only be specified during initialization.
84 107 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
85 108 help="""
86 109 The type of underlying text widget to use. Valid values are 'plain',
87 110 which specifies a QPlainTextEdit, and 'rich', which specifies a
88 111 QTextEdit.
89 112 """
90 113 )
91 114 # NOTE: this value can only be specified during initialization.
92 115 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
93 116 default_value='inside', config=True,
94 117 help="""
95 118 The type of paging to use. Valid values are:
96 119
97 120 'inside' : The widget pages like a traditional terminal.
98 121 'hsplit' : When paging is requested, the widget is split
99 122 horizontally. The top pane contains the console, and the
100 123 bottom pane contains the paged text.
101 124 'vsplit' : Similar to 'hsplit', except that a vertical splitter
102 125 used.
103 126 'custom' : No action is taken by the widget beyond emitting a
104 127 'custom_page_requested(str)' signal.
105 128 'none' : The text is written directly to the console.
106 129 """)
107 130
108 131 font_family = Unicode(config=True,
109 132 help="""The font family to use for the console.
110 133 On OSX this defaults to Monaco, on Windows the default is
111 134 Consolas with fallback of Courier, and on other platforms
112 135 the default is Monospace.
113 136 """)
114 137 def _font_family_default(self):
115 138 if sys.platform == 'win32':
116 139 # Consolas ships with Vista/Win7, fallback to Courier if needed
117 140 return 'Consolas'
118 141 elif sys.platform == 'darwin':
119 142 # OSX always has Monaco, no need for a fallback
120 143 return 'Monaco'
121 144 else:
122 145 # Monospace should always exist, no need for a fallback
123 146 return 'Monospace'
124 147
125 148 font_size = Integer(config=True,
126 149 help="""The font size. If unconfigured, Qt will be entrusted
127 150 with the size of the font.
128 151 """)
129 152
130 153 # Whether to override ShortcutEvents for the keybindings defined by this
131 154 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
132 155 # priority (when it has focus) over, e.g., window-level menu shortcuts.
133 156 override_shortcuts = Bool(False)
134 157
135 158 #------ Signals ------------------------------------------------------------
136 159
137 160 # Signals that indicate ConsoleWidget state.
138 161 copy_available = QtCore.Signal(bool)
139 162 redo_available = QtCore.Signal(bool)
140 163 undo_available = QtCore.Signal(bool)
141 164
142 165 # Signal emitted when paging is needed and the paging style has been
143 166 # specified as 'custom'.
144 167 custom_page_requested = QtCore.Signal(object)
145 168
146 169 # Signal emitted when the font is changed.
147 170 font_changed = QtCore.Signal(QtGui.QFont)
148 171
149 172 #------ Protected class variables ------------------------------------------
150 173
151 174 # control handles
152 175 _control = None
153 176 _page_control = None
154 177 _splitter = None
155 178
156 179 # When the control key is down, these keys are mapped.
157 180 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
158 181 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
159 182 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
160 183 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
161 184 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
162 185 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
163 186 if not sys.platform == 'darwin':
164 187 # On OS X, Ctrl-E already does the right thing, whereas End moves the
165 188 # cursor to the bottom of the buffer.
166 189 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
167 190
168 191 # The shortcuts defined by this widget. We need to keep track of these to
169 192 # support 'override_shortcuts' above.
170 193 _shortcuts = set(_ctrl_down_remap.keys() +
171 194 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
172 195 QtCore.Qt.Key_V ])
173 196
174 197 _temp_buffer_filled = False
175 198
176 199 #---------------------------------------------------------------------------
177 200 # 'QObject' interface
178 201 #---------------------------------------------------------------------------
179 202
180 203 def __init__(self, parent=None, **kw):
181 204 """ Create a ConsoleWidget.
182 205
183 206 Parameters:
184 207 -----------
185 208 parent : QWidget, optional [default None]
186 209 The parent for this widget.
187 210 """
188 211 QtGui.QWidget.__init__(self, parent)
189 212 LoggingConfigurable.__init__(self, **kw)
190 213
191 214 # While scrolling the pager on Mac OS X, it tears badly. The
192 215 # NativeGesture is platform and perhaps build-specific hence
193 216 # we take adequate precautions here.
194 217 self._pager_scroll_events = [QtCore.QEvent.Wheel]
195 218 if hasattr(QtCore.QEvent, 'NativeGesture'):
196 219 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
197 220
198 221 # Create the layout and underlying text widget.
199 222 layout = QtGui.QStackedLayout(self)
200 223 layout.setContentsMargins(0, 0, 0, 0)
201 224 self._control = self._create_control()
202 225 if self.paging in ('hsplit', 'vsplit'):
203 226 self._splitter = QtGui.QSplitter()
204 227 if self.paging == 'hsplit':
205 228 self._splitter.setOrientation(QtCore.Qt.Horizontal)
206 229 else:
207 230 self._splitter.setOrientation(QtCore.Qt.Vertical)
208 231 self._splitter.addWidget(self._control)
209 232 layout.addWidget(self._splitter)
210 233 else:
211 234 layout.addWidget(self._control)
212 235
213 236 # Create the paging widget, if necessary.
214 237 if self.paging in ('inside', 'hsplit', 'vsplit'):
215 238 self._page_control = self._create_page_control()
216 239 if self._splitter:
217 240 self._page_control.hide()
218 241 self._splitter.addWidget(self._page_control)
219 242 else:
220 243 layout.addWidget(self._page_control)
221 244
222 245 # Initialize protected variables. Some variables contain useful state
223 246 # information for subclasses; they should be considered read-only.
224 247 self._append_before_prompt_pos = 0
225 248 self._ansi_processor = QtAnsiCodeProcessor()
226 249 if self.gui_completion == 'ncurses':
227 250 self._completion_widget = CompletionHtml(self)
228 251 elif self.gui_completion == 'droplist':
229 252 self._completion_widget = CompletionWidget(self)
230 253 elif self.gui_completion == 'plain':
231 254 self._completion_widget = CompletionPlain(self)
232 255
233 256 self._continuation_prompt = '> '
234 257 self._continuation_prompt_html = None
235 258 self._executing = False
236 259 self._filter_drag = False
237 260 self._filter_resize = False
238 261 self._html_exporter = HtmlExporter(self._control)
239 262 self._input_buffer_executing = ''
240 263 self._input_buffer_pending = ''
241 264 self._kill_ring = QtKillRing(self._control)
242 265 self._prompt = ''
243 266 self._prompt_html = None
244 267 self._prompt_pos = 0
245 268 self._prompt_sep = ''
246 269 self._reading = False
247 270 self._reading_callback = None
248 271 self._tab_width = 8
249 272
250 273 # Set a monospaced font.
251 274 self.reset_font()
252 275
253 276 # Configure actions.
254 277 action = QtGui.QAction('Print', None)
255 278 action.setEnabled(True)
256 279 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
257 280 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
258 281 # Only override the default if there is a collision.
259 282 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
260 283 printkey = "Ctrl+Shift+P"
261 284 action.setShortcut(printkey)
262 285 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
263 286 action.triggered.connect(self.print_)
264 287 self.addAction(action)
265 288 self.print_action = action
266 289
267 290 action = QtGui.QAction('Save as HTML/XML', None)
268 291 action.setShortcut(QtGui.QKeySequence.Save)
269 292 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
270 293 action.triggered.connect(self.export_html)
271 294 self.addAction(action)
272 295 self.export_action = action
273 296
274 297 action = QtGui.QAction('Select All', None)
275 298 action.setEnabled(True)
276 299 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
277 300 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
278 301 # Only override the default if there is a collision.
279 302 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
280 303 selectall = "Ctrl+Shift+A"
281 304 action.setShortcut(selectall)
282 305 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
283 306 action.triggered.connect(self.select_all)
284 307 self.addAction(action)
285 308 self.select_all_action = action
286 309
287 310 self.increase_font_size = QtGui.QAction("Bigger Font",
288 311 self,
289 312 shortcut=QtGui.QKeySequence.ZoomIn,
290 313 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
291 314 statusTip="Increase the font size by one point",
292 315 triggered=self._increase_font_size)
293 316 self.addAction(self.increase_font_size)
294 317
295 318 self.decrease_font_size = QtGui.QAction("Smaller Font",
296 319 self,
297 320 shortcut=QtGui.QKeySequence.ZoomOut,
298 321 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
299 322 statusTip="Decrease the font size by one point",
300 323 triggered=self._decrease_font_size)
301 324 self.addAction(self.decrease_font_size)
302 325
303 326 self.reset_font_size = QtGui.QAction("Normal Font",
304 327 self,
305 328 shortcut="Ctrl+0",
306 329 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
307 330 statusTip="Restore the Normal font size",
308 331 triggered=self.reset_font)
309 332 self.addAction(self.reset_font_size)
310 333
311 334
312 335
313 336 def eventFilter(self, obj, event):
314 337 """ Reimplemented to ensure a console-like behavior in the underlying
315 338 text widgets.
316 339 """
317 340 etype = event.type()
318 341 if etype == QtCore.QEvent.KeyPress:
319 342
320 343 # Re-map keys for all filtered widgets.
321 344 key = event.key()
322 345 if self._control_key_down(event.modifiers()) and \
323 346 key in self._ctrl_down_remap:
324 347 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
325 348 self._ctrl_down_remap[key],
326 349 QtCore.Qt.NoModifier)
327 350 QtGui.qApp.sendEvent(obj, new_event)
328 351 return True
329 352
330 353 elif obj == self._control:
331 354 return self._event_filter_console_keypress(event)
332 355
333 356 elif obj == self._page_control:
334 357 return self._event_filter_page_keypress(event)
335 358
336 359 # Make middle-click paste safe.
337 360 elif etype == QtCore.QEvent.MouseButtonRelease and \
338 361 event.button() == QtCore.Qt.MidButton and \
339 362 obj == self._control.viewport():
340 363 cursor = self._control.cursorForPosition(event.pos())
341 364 self._control.setTextCursor(cursor)
342 365 self.paste(QtGui.QClipboard.Selection)
343 366 return True
344 367
345 368 # Manually adjust the scrollbars *after* a resize event is dispatched.
346 369 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
347 370 self._filter_resize = True
348 371 QtGui.qApp.sendEvent(obj, event)
349 372 self._adjust_scrollbars()
350 373 self._filter_resize = False
351 374 return True
352 375
353 376 # Override shortcuts for all filtered widgets.
354 377 elif etype == QtCore.QEvent.ShortcutOverride and \
355 378 self.override_shortcuts and \
356 379 self._control_key_down(event.modifiers()) and \
357 380 event.key() in self._shortcuts:
358 381 event.accept()
359 382
360 383 # Ensure that drags are safe. The problem is that the drag starting
361 384 # logic, which determines whether the drag is a Copy or Move, is locked
362 385 # down in QTextControl. If the widget is editable, which it must be if
363 386 # we're not executing, the drag will be a Move. The following hack
364 387 # prevents QTextControl from deleting the text by clearing the selection
365 388 # when a drag leave event originating from this widget is dispatched.
366 389 # The fact that we have to clear the user's selection is unfortunate,
367 390 # but the alternative--trying to prevent Qt from using its hardwired
368 391 # drag logic and writing our own--is worse.
369 392 elif etype == QtCore.QEvent.DragEnter and \
370 393 obj == self._control.viewport() and \
371 394 event.source() == self._control.viewport():
372 395 self._filter_drag = True
373 396 elif etype == QtCore.QEvent.DragLeave and \
374 397 obj == self._control.viewport() and \
375 398 self._filter_drag:
376 399 cursor = self._control.textCursor()
377 400 cursor.clearSelection()
378 401 self._control.setTextCursor(cursor)
379 402 self._filter_drag = False
380 403
381 404 # Ensure that drops are safe.
382 405 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
383 406 cursor = self._control.cursorForPosition(event.pos())
384 407 if self._in_buffer(cursor.position()):
385 408 text = event.mimeData().text()
386 409 self._insert_plain_text_into_buffer(cursor, text)
387 410
388 411 # Qt is expecting to get something here--drag and drop occurs in its
389 412 # own event loop. Send a DragLeave event to end it.
390 413 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
391 414 return True
392 415
393 416 # Handle scrolling of the vsplit pager. This hack attempts to solve
394 417 # problems with tearing of the help text inside the pager window. This
395 418 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
396 419 # perfect but makes the pager more usable.
397 420 elif etype in self._pager_scroll_events and \
398 421 obj == self._page_control:
399 422 self._page_control.repaint()
400 423 return True
401 424 return super(ConsoleWidget, self).eventFilter(obj, event)
402 425
403 426 #---------------------------------------------------------------------------
404 427 # 'QWidget' interface
405 428 #---------------------------------------------------------------------------
406 429
407 430 def sizeHint(self):
408 431 """ Reimplemented to suggest a size that is 80 characters wide and
409 432 25 lines high.
410 433 """
411 434 font_metrics = QtGui.QFontMetrics(self.font)
412 435 margin = (self._control.frameWidth() +
413 436 self._control.document().documentMargin()) * 2
414 437 style = self.style()
415 438 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
416 439
417 440 # Note 1: Despite my best efforts to take the various margins into
418 441 # account, the width is still coming out a bit too small, so we include
419 442 # a fudge factor of one character here.
420 443 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
421 444 # to a Qt bug on certain Mac OS systems where it returns 0.
422 445 width = font_metrics.width(' ') * 81 + margin
423 446 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
424 447 if self.paging == 'hsplit':
425 448 width = width * 2 + splitwidth
426 449
427 450 height = font_metrics.height() * 25 + margin
428 451 if self.paging == 'vsplit':
429 452 height = height * 2 + splitwidth
430 453
431 454 return QtCore.QSize(width, height)
432 455
433 456 #---------------------------------------------------------------------------
434 457 # 'ConsoleWidget' public interface
435 458 #---------------------------------------------------------------------------
436 459
437 460 def can_copy(self):
438 461 """ Returns whether text can be copied to the clipboard.
439 462 """
440 463 return self._control.textCursor().hasSelection()
441 464
442 465 def can_cut(self):
443 466 """ Returns whether text can be cut to the clipboard.
444 467 """
445 468 cursor = self._control.textCursor()
446 469 return (cursor.hasSelection() and
447 470 self._in_buffer(cursor.anchor()) and
448 471 self._in_buffer(cursor.position()))
449 472
450 473 def can_paste(self):
451 474 """ Returns whether text can be pasted from the clipboard.
452 475 """
453 476 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
454 477 return bool(QtGui.QApplication.clipboard().text())
455 478 return False
456 479
457 480 def clear(self, keep_input=True):
458 481 """ Clear the console.
459 482
460 483 Parameters:
461 484 -----------
462 485 keep_input : bool, optional (default True)
463 486 If set, restores the old input buffer if a new prompt is written.
464 487 """
465 488 if self._executing:
466 489 self._control.clear()
467 490 else:
468 491 if keep_input:
469 492 input_buffer = self.input_buffer
470 493 self._control.clear()
471 494 self._show_prompt()
472 495 if keep_input:
473 496 self.input_buffer = input_buffer
474 497
475 498 def copy(self):
476 499 """ Copy the currently selected text to the clipboard.
477 500 """
478 501 self.layout().currentWidget().copy()
479 502
480 503 def cut(self):
481 504 """ Copy the currently selected text to the clipboard and delete it
482 505 if it's inside the input buffer.
483 506 """
484 507 self.copy()
485 508 if self.can_cut():
486 509 self._control.textCursor().removeSelectedText()
487 510
488 511 def execute(self, source=None, hidden=False, interactive=False):
489 512 """ Executes source or the input buffer, possibly prompting for more
490 513 input.
491 514
492 515 Parameters:
493 516 -----------
494 517 source : str, optional
495 518
496 519 The source to execute. If not specified, the input buffer will be
497 520 used. If specified and 'hidden' is False, the input buffer will be
498 521 replaced with the source before execution.
499 522
500 523 hidden : bool, optional (default False)
501 524
502 525 If set, no output will be shown and the prompt will not be modified.
503 526 In other words, it will be completely invisible to the user that
504 527 an execution has occurred.
505 528
506 529 interactive : bool, optional (default False)
507 530
508 531 Whether the console is to treat the source as having been manually
509 532 entered by the user. The effect of this parameter depends on the
510 533 subclass implementation.
511 534
512 535 Raises:
513 536 -------
514 537 RuntimeError
515 538 If incomplete input is given and 'hidden' is True. In this case,
516 539 it is not possible to prompt for more input.
517 540
518 541 Returns:
519 542 --------
520 543 A boolean indicating whether the source was executed.
521 544 """
522 545 # WARNING: The order in which things happen here is very particular, in
523 546 # large part because our syntax highlighting is fragile. If you change
524 547 # something, test carefully!
525 548
526 549 # Decide what to execute.
527 550 if source is None:
528 551 source = self.input_buffer
529 552 if not hidden:
530 553 # A newline is appended later, but it should be considered part
531 554 # of the input buffer.
532 555 source += '\n'
533 556 elif not hidden:
534 557 self.input_buffer = source
535 558
536 559 # Execute the source or show a continuation prompt if it is incomplete.
537 560 complete = self._is_complete(source, interactive)
538 561 if hidden:
539 562 if complete:
540 563 self._execute(source, hidden)
541 564 else:
542 565 error = 'Incomplete noninteractive input: "%s"'
543 566 raise RuntimeError(error % source)
544 567 else:
545 568 if complete:
546 569 self._append_plain_text('\n')
547 570 self._input_buffer_executing = self.input_buffer
548 571 self._executing = True
549 572 self._prompt_finished()
550 573
551 574 # The maximum block count is only in effect during execution.
552 575 # This ensures that _prompt_pos does not become invalid due to
553 576 # text truncation.
554 577 self._control.document().setMaximumBlockCount(self.buffer_size)
555 578
556 579 # Setting a positive maximum block count will automatically
557 580 # disable the undo/redo history, but just to be safe:
558 581 self._control.setUndoRedoEnabled(False)
559 582
560 583 # Perform actual execution.
561 584 self._execute(source, hidden)
562 585
563 586 else:
564 587 # Do this inside an edit block so continuation prompts are
565 588 # removed seamlessly via undo/redo.
566 589 cursor = self._get_end_cursor()
567 590 cursor.beginEditBlock()
568 591 cursor.insertText('\n')
569 592 self._insert_continuation_prompt(cursor)
570 593 cursor.endEditBlock()
571 594
572 595 # Do not do this inside the edit block. It works as expected
573 596 # when using a QPlainTextEdit control, but does not have an
574 597 # effect when using a QTextEdit. I believe this is a Qt bug.
575 598 self._control.moveCursor(QtGui.QTextCursor.End)
576 599
577 600 return complete
578 601
579 602 def export_html(self):
580 603 """ Shows a dialog to export HTML/XML in various formats.
581 604 """
582 605 self._html_exporter.export()
583 606
584 607 def _get_input_buffer(self, force=False):
585 608 """ The text that the user has entered entered at the current prompt.
586 609
587 610 If the console is currently executing, the text that is executing will
588 611 always be returned.
589 612 """
590 613 # If we're executing, the input buffer may not even exist anymore due to
591 614 # the limit imposed by 'buffer_size'. Therefore, we store it.
592 615 if self._executing and not force:
593 616 return self._input_buffer_executing
594 617
595 618 cursor = self._get_end_cursor()
596 619 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
597 620 input_buffer = cursor.selection().toPlainText()
598 621
599 622 # Strip out continuation prompts.
600 623 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
601 624
602 625 def _set_input_buffer(self, string):
603 626 """ Sets the text in the input buffer.
604 627
605 628 If the console is currently executing, this call has no *immediate*
606 629 effect. When the execution is finished, the input buffer will be updated
607 630 appropriately.
608 631 """
609 632 # If we're executing, store the text for later.
610 633 if self._executing:
611 634 self._input_buffer_pending = string
612 635 return
613 636
614 637 # Remove old text.
615 638 cursor = self._get_end_cursor()
616 639 cursor.beginEditBlock()
617 640 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
618 641 cursor.removeSelectedText()
619 642
620 643 # Insert new text with continuation prompts.
621 644 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
622 645 cursor.endEditBlock()
623 646 self._control.moveCursor(QtGui.QTextCursor.End)
624 647
625 648 input_buffer = property(_get_input_buffer, _set_input_buffer)
626 649
627 650 def _get_font(self):
628 651 """ The base font being used by the ConsoleWidget.
629 652 """
630 653 return self._control.document().defaultFont()
631 654
632 655 def _set_font(self, font):
633 656 """ Sets the base font for the ConsoleWidget to the specified QFont.
634 657 """
635 658 font_metrics = QtGui.QFontMetrics(font)
636 659 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
637 660
638 661 self._completion_widget.setFont(font)
639 662 self._control.document().setDefaultFont(font)
640 663 if self._page_control:
641 664 self._page_control.document().setDefaultFont(font)
642 665
643 666 self.font_changed.emit(font)
644 667
645 668 font = property(_get_font, _set_font)
646 669
647 670 def paste(self, mode=QtGui.QClipboard.Clipboard):
648 671 """ Paste the contents of the clipboard into the input region.
649 672
650 673 Parameters:
651 674 -----------
652 675 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
653 676
654 677 Controls which part of the system clipboard is used. This can be
655 678 used to access the selection clipboard in X11 and the Find buffer
656 679 in Mac OS. By default, the regular clipboard is used.
657 680 """
658 681 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
659 682 # Make sure the paste is safe.
660 683 self._keep_cursor_in_buffer()
661 684 cursor = self._control.textCursor()
662 685
663 686 # Remove any trailing newline, which confuses the GUI and forces the
664 687 # user to backspace.
665 688 text = QtGui.QApplication.clipboard().text(mode).rstrip()
666 689 self._insert_plain_text_into_buffer(cursor, dedent(text))
667 690
668 691 def print_(self, printer = None):
669 692 """ Print the contents of the ConsoleWidget to the specified QPrinter.
670 693 """
671 694 if (not printer):
672 695 printer = QtGui.QPrinter()
673 696 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
674 697 return
675 698 self._control.print_(printer)
676 699
677 700 def prompt_to_top(self):
678 701 """ Moves the prompt to the top of the viewport.
679 702 """
680 703 if not self._executing:
681 704 prompt_cursor = self._get_prompt_cursor()
682 705 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
683 706 self._set_cursor(prompt_cursor)
684 707 self._set_top_cursor(prompt_cursor)
685 708
686 709 def redo(self):
687 710 """ Redo the last operation. If there is no operation to redo, nothing
688 711 happens.
689 712 """
690 713 self._control.redo()
691 714
692 715 def reset_font(self):
693 716 """ Sets the font to the default fixed-width font for this platform.
694 717 """
695 718 if sys.platform == 'win32':
696 719 # Consolas ships with Vista/Win7, fallback to Courier if needed
697 720 fallback = 'Courier'
698 721 elif sys.platform == 'darwin':
699 722 # OSX always has Monaco
700 723 fallback = 'Monaco'
701 724 else:
702 725 # Monospace should always exist
703 726 fallback = 'Monospace'
704 727 font = get_font(self.font_family, fallback)
705 728 if self.font_size:
706 729 font.setPointSize(self.font_size)
707 730 else:
708 731 font.setPointSize(QtGui.qApp.font().pointSize())
709 732 font.setStyleHint(QtGui.QFont.TypeWriter)
710 733 self._set_font(font)
711 734
712 735 def change_font_size(self, delta):
713 736 """Change the font size by the specified amount (in points).
714 737 """
715 738 font = self.font
716 739 size = max(font.pointSize() + delta, 1) # minimum 1 point
717 740 font.setPointSize(size)
718 741 self._set_font(font)
719 742
720 743 def _increase_font_size(self):
721 744 self.change_font_size(1)
722 745
723 746 def _decrease_font_size(self):
724 747 self.change_font_size(-1)
725 748
726 749 def select_all(self):
727 750 """ Selects all the text in the buffer.
728 751 """
729 752 self._control.selectAll()
730 753
731 754 def _get_tab_width(self):
732 755 """ The width (in terms of space characters) for tab characters.
733 756 """
734 757 return self._tab_width
735 758
736 759 def _set_tab_width(self, tab_width):
737 760 """ Sets the width (in terms of space characters) for tab characters.
738 761 """
739 762 font_metrics = QtGui.QFontMetrics(self.font)
740 763 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
741 764
742 765 self._tab_width = tab_width
743 766
744 767 tab_width = property(_get_tab_width, _set_tab_width)
745 768
746 769 def undo(self):
747 770 """ Undo the last operation. If there is no operation to undo, nothing
748 771 happens.
749 772 """
750 773 self._control.undo()
751 774
752 775 #---------------------------------------------------------------------------
753 776 # 'ConsoleWidget' abstract interface
754 777 #---------------------------------------------------------------------------
755 778
756 779 def _is_complete(self, source, interactive):
757 780 """ Returns whether 'source' can be executed. When triggered by an
758 781 Enter/Return key press, 'interactive' is True; otherwise, it is
759 782 False.
760 783 """
761 784 raise NotImplementedError
762 785
763 786 def _execute(self, source, hidden):
764 787 """ Execute 'source'. If 'hidden', do not show any output.
765 788 """
766 789 raise NotImplementedError
767 790
768 791 def _prompt_started_hook(self):
769 792 """ Called immediately after a new prompt is displayed.
770 793 """
771 794 pass
772 795
773 796 def _prompt_finished_hook(self):
774 797 """ Called immediately after a prompt is finished, i.e. when some input
775 798 will be processed and a new prompt displayed.
776 799 """
777 800 pass
778 801
779 802 def _up_pressed(self, shift_modifier):
780 803 """ Called when the up key is pressed. Returns whether to continue
781 804 processing the event.
782 805 """
783 806 return True
784 807
785 808 def _down_pressed(self, shift_modifier):
786 809 """ Called when the down key is pressed. Returns whether to continue
787 810 processing the event.
788 811 """
789 812 return True
790 813
791 814 def _tab_pressed(self):
792 815 """ Called when the tab key is pressed. Returns whether to continue
793 816 processing the event.
794 817 """
795 818 return False
796 819
797 820 #--------------------------------------------------------------------------
798 821 # 'ConsoleWidget' protected interface
799 822 #--------------------------------------------------------------------------
800 823
801 824 def _append_custom(self, insert, input, before_prompt=False):
802 825 """ A low-level method for appending content to the end of the buffer.
803 826
804 827 If 'before_prompt' is enabled, the content will be inserted before the
805 828 current prompt, if there is one.
806 829 """
807 830 # Determine where to insert the content.
808 831 cursor = self._control.textCursor()
809 832 if before_prompt and (self._reading or not self._executing):
810 833 cursor.setPosition(self._append_before_prompt_pos)
811 834 else:
812 835 cursor.movePosition(QtGui.QTextCursor.End)
813 836 start_pos = cursor.position()
814 837
815 838 # Perform the insertion.
816 839 result = insert(cursor, input)
817 840
818 841 # Adjust the prompt position if we have inserted before it. This is safe
819 842 # because buffer truncation is disabled when not executing.
820 843 if before_prompt and not self._executing:
821 844 diff = cursor.position() - start_pos
822 845 self._append_before_prompt_pos += diff
823 846 self._prompt_pos += diff
824 847
825 848 return result
826 849
827 850 def _append_html(self, html, before_prompt=False):
828 851 """ Appends HTML at the end of the console buffer.
829 852 """
830 853 self._append_custom(self._insert_html, html, before_prompt)
831 854
832 855 def _append_html_fetching_plain_text(self, html, before_prompt=False):
833 856 """ Appends HTML, then returns the plain text version of it.
834 857 """
835 858 return self._append_custom(self._insert_html_fetching_plain_text,
836 859 html, before_prompt)
837 860
838 861 def _append_plain_text(self, text, before_prompt=False):
839 862 """ Appends plain text, processing ANSI codes if enabled.
840 863 """
841 864 self._append_custom(self._insert_plain_text, text, before_prompt)
842 865
843 866 def _cancel_completion(self):
844 867 """ If text completion is progress, cancel it.
845 868 """
846 869 self._completion_widget.cancel_completion()
847 870
848 871 def _clear_temporary_buffer(self):
849 872 """ Clears the "temporary text" buffer, i.e. all the text following
850 873 the prompt region.
851 874 """
852 875 # Select and remove all text below the input buffer.
853 876 _temp_buffer_filled = False
854 877 cursor = self._get_prompt_cursor()
855 878 prompt = self._continuation_prompt.lstrip()
856 879 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
857 880 temp_cursor = QtGui.QTextCursor(cursor)
858 881 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
859 882 text = temp_cursor.selection().toPlainText().lstrip()
860 883 if not text.startswith(prompt):
861 884 break
862 885 else:
863 886 # We've reached the end of the input buffer and no text follows.
864 887 return
865 888 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
866 889 cursor.movePosition(QtGui.QTextCursor.End,
867 890 QtGui.QTextCursor.KeepAnchor)
868 891 cursor.removeSelectedText()
869 892
870 893 # After doing this, we have no choice but to clear the undo/redo
871 894 # history. Otherwise, the text is not "temporary" at all, because it
872 895 # can be recalled with undo/redo. Unfortunately, Qt does not expose
873 896 # fine-grained control to the undo/redo system.
874 897 if self._control.isUndoRedoEnabled():
875 898 self._control.setUndoRedoEnabled(False)
876 899 self._control.setUndoRedoEnabled(True)
877 900
878 901 def _complete_with_items(self, cursor, items):
879 902 """ Performs completion with 'items' at the specified cursor location.
880 903 """
881 904 self._cancel_completion()
882 905
883 906 if len(items) == 1:
884 907 cursor.setPosition(self._control.textCursor().position(),
885 908 QtGui.QTextCursor.KeepAnchor)
886 909 cursor.insertText(items[0])
887 910
888 911 elif len(items) > 1:
889 912 current_pos = self._control.textCursor().position()
890 913 prefix = commonprefix(items)
891 914 if prefix:
892 915 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
893 916 cursor.insertText(prefix)
894 917 current_pos = cursor.position()
895 918
896 919 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
897 920 self._completion_widget.show_items(cursor, items)
898 921
899 922
900 923 def _fill_temporary_buffer(self, cursor, text, html=False):
901 924 """fill the area below the active editting zone with text"""
902 925
903 926 current_pos = self._control.textCursor().position()
904 927
905 928 cursor.beginEditBlock()
906 929 self._append_plain_text('\n')
907 930 self._page(text, html=html)
908 931 cursor.endEditBlock()
909 932
910 933 cursor.setPosition(current_pos)
911 934 self._control.moveCursor(QtGui.QTextCursor.End)
912 935 self._control.setTextCursor(cursor)
913 936
914 937 _temp_buffer_filled = True
915 938
916 939
917 940 def _context_menu_make(self, pos):
918 941 """ Creates a context menu for the given QPoint (in widget coordinates).
919 942 """
920 943 menu = QtGui.QMenu(self)
921 944
922 945 self.cut_action = menu.addAction('Cut', self.cut)
923 946 self.cut_action.setEnabled(self.can_cut())
924 947 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
925 948
926 949 self.copy_action = menu.addAction('Copy', self.copy)
927 950 self.copy_action.setEnabled(self.can_copy())
928 951 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
929 952
930 953 self.paste_action = menu.addAction('Paste', self.paste)
931 954 self.paste_action.setEnabled(self.can_paste())
932 955 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
933 956
934 957 menu.addSeparator()
935 958 menu.addAction(self.select_all_action)
936 959
937 960 menu.addSeparator()
938 961 menu.addAction(self.export_action)
939 962 menu.addAction(self.print_action)
940 963
941 964 return menu
942 965
943 966 def _control_key_down(self, modifiers, include_command=False):
944 967 """ Given a KeyboardModifiers flags object, return whether the Control
945 968 key is down.
946 969
947 970 Parameters:
948 971 -----------
949 972 include_command : bool, optional (default True)
950 973 Whether to treat the Command key as a (mutually exclusive) synonym
951 974 for Control when in Mac OS.
952 975 """
953 976 # Note that on Mac OS, ControlModifier corresponds to the Command key
954 977 # while MetaModifier corresponds to the Control key.
955 978 if sys.platform == 'darwin':
956 979 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
957 980 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
958 981 else:
959 982 return bool(modifiers & QtCore.Qt.ControlModifier)
960 983
961 984 def _create_control(self):
962 985 """ Creates and connects the underlying text widget.
963 986 """
964 987 # Create the underlying control.
965 988 if self.kind == 'plain':
966 989 control = QtGui.QPlainTextEdit()
967 990 elif self.kind == 'rich':
968 991 control = QtGui.QTextEdit()
969 992 control.setAcceptRichText(False)
970 993
971 994 # Install event filters. The filter on the viewport is needed for
972 995 # mouse events and drag events.
973 996 control.installEventFilter(self)
974 997 control.viewport().installEventFilter(self)
975 998
976 999 # Connect signals.
977 1000 control.customContextMenuRequested.connect(
978 1001 self._custom_context_menu_requested)
979 1002 control.copyAvailable.connect(self.copy_available)
980 1003 control.redoAvailable.connect(self.redo_available)
981 1004 control.undoAvailable.connect(self.undo_available)
982 1005
983 1006 # Hijack the document size change signal to prevent Qt from adjusting
984 1007 # the viewport's scrollbar. We are relying on an implementation detail
985 1008 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
986 1009 # this functionality we cannot create a nice terminal interface.
987 1010 layout = control.document().documentLayout()
988 1011 layout.documentSizeChanged.disconnect()
989 1012 layout.documentSizeChanged.connect(self._adjust_scrollbars)
990 1013
991 1014 # Configure the control.
992 1015 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
993 1016 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
994 1017 control.setReadOnly(True)
995 1018 control.setUndoRedoEnabled(False)
996 1019 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
997 1020 return control
998 1021
999 1022 def _create_page_control(self):
1000 1023 """ Creates and connects the underlying paging widget.
1001 1024 """
1002 1025 if self.kind == 'plain':
1003 1026 control = QtGui.QPlainTextEdit()
1004 1027 elif self.kind == 'rich':
1005 1028 control = QtGui.QTextEdit()
1006 1029 control.installEventFilter(self)
1007 1030 viewport = control.viewport()
1008 1031 viewport.installEventFilter(self)
1009 1032 control.setReadOnly(True)
1010 1033 control.setUndoRedoEnabled(False)
1011 1034 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1012 1035 return control
1013 1036
1014 1037 def _event_filter_console_keypress(self, event):
1015 1038 """ Filter key events for the underlying text widget to create a
1016 1039 console-like interface.
1017 1040 """
1018 1041 intercepted = False
1019 1042 cursor = self._control.textCursor()
1020 1043 position = cursor.position()
1021 1044 key = event.key()
1022 1045 ctrl_down = self._control_key_down(event.modifiers())
1023 1046 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1024 1047 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1025 1048
1026 1049 #------ Special sequences ----------------------------------------------
1027 1050
1028 1051 if event.matches(QtGui.QKeySequence.Copy):
1029 1052 self.copy()
1030 1053 intercepted = True
1031 1054
1032 1055 elif event.matches(QtGui.QKeySequence.Cut):
1033 1056 self.cut()
1034 1057 intercepted = True
1035 1058
1036 1059 elif event.matches(QtGui.QKeySequence.Paste):
1037 1060 self.paste()
1038 1061 intercepted = True
1039 1062
1040 1063 #------ Special modifier logic -----------------------------------------
1041 1064
1042 1065 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1043 1066 intercepted = True
1044 1067
1045 1068 # Special handling when tab completing in text mode.
1046 1069 self._cancel_completion()
1047 1070
1048 1071 if self._in_buffer(position):
1049 1072 # Special handling when a reading a line of raw input.
1050 1073 if self._reading:
1051 1074 self._append_plain_text('\n')
1052 1075 self._reading = False
1053 1076 if self._reading_callback:
1054 1077 self._reading_callback()
1055 1078
1056 1079 # If the input buffer is a single line or there is only
1057 1080 # whitespace after the cursor, execute. Otherwise, split the
1058 1081 # line with a continuation prompt.
1059 1082 elif not self._executing:
1060 1083 cursor.movePosition(QtGui.QTextCursor.End,
1061 1084 QtGui.QTextCursor.KeepAnchor)
1062 1085 at_end = len(cursor.selectedText().strip()) == 0
1063 1086 single_line = (self._get_end_cursor().blockNumber() ==
1064 1087 self._get_prompt_cursor().blockNumber())
1065 1088 if (at_end or shift_down or single_line) and not ctrl_down:
1066 1089 self.execute(interactive = not shift_down)
1067 1090 else:
1068 1091 # Do this inside an edit block for clean undo/redo.
1069 1092 cursor.beginEditBlock()
1070 1093 cursor.setPosition(position)
1071 1094 cursor.insertText('\n')
1072 1095 self._insert_continuation_prompt(cursor)
1073 1096 cursor.endEditBlock()
1074 1097
1075 1098 # Ensure that the whole input buffer is visible.
1076 1099 # FIXME: This will not be usable if the input buffer is
1077 1100 # taller than the console widget.
1078 1101 self._control.moveCursor(QtGui.QTextCursor.End)
1079 1102 self._control.setTextCursor(cursor)
1080 1103
1081 1104 #------ Control/Cmd modifier -------------------------------------------
1082 1105
1083 1106 elif ctrl_down:
1084 1107 if key == QtCore.Qt.Key_G:
1085 1108 self._keyboard_quit()
1086 1109 intercepted = True
1087 1110
1088 1111 elif key == QtCore.Qt.Key_K:
1089 1112 if self._in_buffer(position):
1090 1113 cursor.clearSelection()
1091 1114 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1092 1115 QtGui.QTextCursor.KeepAnchor)
1093 1116 if not cursor.hasSelection():
1094 1117 # Line deletion (remove continuation prompt)
1095 1118 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1096 1119 QtGui.QTextCursor.KeepAnchor)
1097 1120 cursor.movePosition(QtGui.QTextCursor.Right,
1098 1121 QtGui.QTextCursor.KeepAnchor,
1099 1122 len(self._continuation_prompt))
1100 1123 self._kill_ring.kill_cursor(cursor)
1101 1124 self._set_cursor(cursor)
1102 1125 intercepted = True
1103 1126
1104 1127 elif key == QtCore.Qt.Key_L:
1105 1128 self.prompt_to_top()
1106 1129 intercepted = True
1107 1130
1108 1131 elif key == QtCore.Qt.Key_O:
1109 1132 if self._page_control and self._page_control.isVisible():
1110 1133 self._page_control.setFocus()
1111 1134 intercepted = True
1112 1135
1113 1136 elif key == QtCore.Qt.Key_U:
1114 1137 if self._in_buffer(position):
1115 1138 cursor.clearSelection()
1116 1139 start_line = cursor.blockNumber()
1117 1140 if start_line == self._get_prompt_cursor().blockNumber():
1118 1141 offset = len(self._prompt)
1119 1142 else:
1120 1143 offset = len(self._continuation_prompt)
1121 1144 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1122 1145 QtGui.QTextCursor.KeepAnchor)
1123 1146 cursor.movePosition(QtGui.QTextCursor.Right,
1124 1147 QtGui.QTextCursor.KeepAnchor, offset)
1125 1148 self._kill_ring.kill_cursor(cursor)
1126 1149 self._set_cursor(cursor)
1127 1150 intercepted = True
1128 1151
1129 1152 elif key == QtCore.Qt.Key_Y:
1130 1153 self._keep_cursor_in_buffer()
1131 1154 self._kill_ring.yank()
1132 1155 intercepted = True
1133 1156
1134 1157 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1135 1158 if key == QtCore.Qt.Key_Backspace:
1136 1159 cursor = self._get_word_start_cursor(position)
1137 1160 else: # key == QtCore.Qt.Key_Delete
1138 1161 cursor = self._get_word_end_cursor(position)
1139 1162 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1140 1163 self._kill_ring.kill_cursor(cursor)
1141 1164 intercepted = True
1142 1165
1143 1166 elif key == QtCore.Qt.Key_D:
1144 1167 if len(self.input_buffer) == 0:
1145 1168 self.exit_requested.emit(self)
1146 1169 else:
1147 1170 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1148 1171 QtCore.Qt.Key_Delete,
1149 1172 QtCore.Qt.NoModifier)
1150 1173 QtGui.qApp.sendEvent(self._control, new_event)
1151 1174 intercepted = True
1152 1175
1153 1176 #------ Alt modifier ---------------------------------------------------
1154 1177
1155 1178 elif alt_down:
1156 1179 if key == QtCore.Qt.Key_B:
1157 1180 self._set_cursor(self._get_word_start_cursor(position))
1158 1181 intercepted = True
1159 1182
1160 1183 elif key == QtCore.Qt.Key_F:
1161 1184 self._set_cursor(self._get_word_end_cursor(position))
1162 1185 intercepted = True
1163 1186
1164 1187 elif key == QtCore.Qt.Key_Y:
1165 1188 self._kill_ring.rotate()
1166 1189 intercepted = True
1167 1190
1168 1191 elif key == QtCore.Qt.Key_Backspace:
1169 1192 cursor = self._get_word_start_cursor(position)
1170 1193 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1171 1194 self._kill_ring.kill_cursor(cursor)
1172 1195 intercepted = True
1173 1196
1174 1197 elif key == QtCore.Qt.Key_D:
1175 1198 cursor = self._get_word_end_cursor(position)
1176 1199 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1177 1200 self._kill_ring.kill_cursor(cursor)
1178 1201 intercepted = True
1179 1202
1180 1203 elif key == QtCore.Qt.Key_Delete:
1181 1204 intercepted = True
1182 1205
1183 1206 elif key == QtCore.Qt.Key_Greater:
1184 1207 self._control.moveCursor(QtGui.QTextCursor.End)
1185 1208 intercepted = True
1186 1209
1187 1210 elif key == QtCore.Qt.Key_Less:
1188 1211 self._control.setTextCursor(self._get_prompt_cursor())
1189 1212 intercepted = True
1190 1213
1191 1214 #------ No modifiers ---------------------------------------------------
1192 1215
1193 1216 else:
1194 1217 if shift_down:
1195 1218 anchormode = QtGui.QTextCursor.KeepAnchor
1196 1219 else:
1197 1220 anchormode = QtGui.QTextCursor.MoveAnchor
1198 1221
1199 1222 if key == QtCore.Qt.Key_Escape:
1200 1223 self._keyboard_quit()
1201 1224 intercepted = True
1202 1225
1203 1226 elif key == QtCore.Qt.Key_Up:
1204 1227 if self._reading or not self._up_pressed(shift_down):
1205 1228 intercepted = True
1206 1229 else:
1207 1230 prompt_line = self._get_prompt_cursor().blockNumber()
1208 1231 intercepted = cursor.blockNumber() <= prompt_line
1209 1232
1210 1233 elif key == QtCore.Qt.Key_Down:
1211 1234 if self._reading or not self._down_pressed(shift_down):
1212 1235 intercepted = True
1213 1236 else:
1214 1237 end_line = self._get_end_cursor().blockNumber()
1215 1238 intercepted = cursor.blockNumber() == end_line
1216 1239
1217 1240 elif key == QtCore.Qt.Key_Tab:
1218 1241 if not self._reading:
1219 1242 if self._tab_pressed():
1220 1243 # real tab-key, insert four spaces
1221 1244 cursor.insertText(' '*4)
1222 1245 intercepted = True
1223 1246
1224 1247 elif key == QtCore.Qt.Key_Left:
1225 1248
1226 1249 # Move to the previous line
1227 1250 line, col = cursor.blockNumber(), cursor.columnNumber()
1228 1251 if line > self._get_prompt_cursor().blockNumber() and \
1229 1252 col == len(self._continuation_prompt):
1230 1253 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1231 1254 mode=anchormode)
1232 1255 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1233 1256 mode=anchormode)
1234 1257 intercepted = True
1235 1258
1236 1259 # Regular left movement
1237 1260 else:
1238 1261 intercepted = not self._in_buffer(position - 1)
1239 1262
1240 1263 elif key == QtCore.Qt.Key_Right:
1241 1264 original_block_number = cursor.blockNumber()
1242 1265 cursor.movePosition(QtGui.QTextCursor.Right,
1243 1266 mode=anchormode)
1244 1267 if cursor.blockNumber() != original_block_number:
1245 1268 cursor.movePosition(QtGui.QTextCursor.Right,
1246 1269 n=len(self._continuation_prompt),
1247 1270 mode=anchormode)
1248 1271 self._set_cursor(cursor)
1249 1272 intercepted = True
1250 1273
1251 1274 elif key == QtCore.Qt.Key_Home:
1252 1275 start_line = cursor.blockNumber()
1253 1276 if start_line == self._get_prompt_cursor().blockNumber():
1254 1277 start_pos = self._prompt_pos
1255 1278 else:
1256 1279 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1257 1280 QtGui.QTextCursor.KeepAnchor)
1258 1281 start_pos = cursor.position()
1259 1282 start_pos += len(self._continuation_prompt)
1260 1283 cursor.setPosition(position)
1261 1284 if shift_down and self._in_buffer(position):
1262 1285 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1263 1286 else:
1264 1287 cursor.setPosition(start_pos)
1265 1288 self._set_cursor(cursor)
1266 1289 intercepted = True
1267 1290
1268 1291 elif key == QtCore.Qt.Key_Backspace:
1269 1292
1270 1293 # Line deletion (remove continuation prompt)
1271 1294 line, col = cursor.blockNumber(), cursor.columnNumber()
1272 1295 if not self._reading and \
1273 1296 col == len(self._continuation_prompt) and \
1274 1297 line > self._get_prompt_cursor().blockNumber():
1275 1298 cursor.beginEditBlock()
1276 1299 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1277 1300 QtGui.QTextCursor.KeepAnchor)
1278 1301 cursor.removeSelectedText()
1279 1302 cursor.deletePreviousChar()
1280 1303 cursor.endEditBlock()
1281 1304 intercepted = True
1282 1305
1283 1306 # Regular backwards deletion
1284 1307 else:
1285 1308 anchor = cursor.anchor()
1286 1309 if anchor == position:
1287 1310 intercepted = not self._in_buffer(position - 1)
1288 1311 else:
1289 1312 intercepted = not self._in_buffer(min(anchor, position))
1290 1313
1291 1314 elif key == QtCore.Qt.Key_Delete:
1292 1315
1293 1316 # Line deletion (remove continuation prompt)
1294 1317 if not self._reading and self._in_buffer(position) and \
1295 1318 cursor.atBlockEnd() and not cursor.hasSelection():
1296 1319 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1297 1320 QtGui.QTextCursor.KeepAnchor)
1298 1321 cursor.movePosition(QtGui.QTextCursor.Right,
1299 1322 QtGui.QTextCursor.KeepAnchor,
1300 1323 len(self._continuation_prompt))
1301 1324 cursor.removeSelectedText()
1302 1325 intercepted = True
1303 1326
1304 1327 # Regular forwards deletion:
1305 1328 else:
1306 1329 anchor = cursor.anchor()
1307 1330 intercepted = (not self._in_buffer(anchor) or
1308 1331 not self._in_buffer(position))
1309 1332
1310 1333 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1311 1334 # using the keyboard in any part of the buffer. Also, permit scrolling
1312 1335 # with Page Up/Down keys. Finally, if we're executing, don't move the
1313 1336 # cursor (if even this made sense, we can't guarantee that the prompt
1314 1337 # position is still valid due to text truncation).
1315 1338 if not (self._control_key_down(event.modifiers(), include_command=True)
1316 1339 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1317 1340 or (self._executing and not self._reading)):
1318 1341 self._keep_cursor_in_buffer()
1319 1342
1320 1343 return intercepted
1321 1344
1322 1345 def _event_filter_page_keypress(self, event):
1323 1346 """ Filter key events for the paging widget to create console-like
1324 1347 interface.
1325 1348 """
1326 1349 key = event.key()
1327 1350 ctrl_down = self._control_key_down(event.modifiers())
1328 1351 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1329 1352
1330 1353 if ctrl_down:
1331 1354 if key == QtCore.Qt.Key_O:
1332 1355 self._control.setFocus()
1333 1356 intercept = True
1334 1357
1335 1358 elif alt_down:
1336 1359 if key == QtCore.Qt.Key_Greater:
1337 1360 self._page_control.moveCursor(QtGui.QTextCursor.End)
1338 1361 intercepted = True
1339 1362
1340 1363 elif key == QtCore.Qt.Key_Less:
1341 1364 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1342 1365 intercepted = True
1343 1366
1344 1367 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1345 1368 if self._splitter:
1346 1369 self._page_control.hide()
1347 1370 self._control.setFocus()
1348 1371 else:
1349 1372 self.layout().setCurrentWidget(self._control)
1350 1373 return True
1351 1374
1352 1375 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1353 1376 QtCore.Qt.Key_Tab):
1354 1377 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1355 1378 QtCore.Qt.Key_PageDown,
1356 1379 QtCore.Qt.NoModifier)
1357 1380 QtGui.qApp.sendEvent(self._page_control, new_event)
1358 1381 return True
1359 1382
1360 1383 elif key == QtCore.Qt.Key_Backspace:
1361 1384 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1362 1385 QtCore.Qt.Key_PageUp,
1363 1386 QtCore.Qt.NoModifier)
1364 1387 QtGui.qApp.sendEvent(self._page_control, new_event)
1365 1388 return True
1366 1389
1367 1390 return False
1368 1391
1369 1392 def _format_as_columns(self, items, separator=' '):
1370 1393 """ Transform a list of strings into a single string with columns.
1371 1394
1372 1395 Parameters
1373 1396 ----------
1374 1397 items : sequence of strings
1375 1398 The strings to process.
1376 1399
1377 1400 separator : str, optional [default is two spaces]
1378 1401 The string that separates columns.
1379 1402
1380 1403 Returns
1381 1404 -------
1382 1405 The formatted string.
1383 1406 """
1384 1407 # Calculate the number of characters available.
1385 1408 width = self._control.viewport().width()
1386 1409 char_width = QtGui.QFontMetrics(self.font).width(' ')
1387 1410 displaywidth = max(10, (width / char_width) - 1)
1388 1411
1389 1412 return columnize(items, separator, displaywidth)
1390 1413
1391 1414 def _get_block_plain_text(self, block):
1392 1415 """ Given a QTextBlock, return its unformatted text.
1393 1416 """
1394 1417 cursor = QtGui.QTextCursor(block)
1395 1418 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1396 1419 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1397 1420 QtGui.QTextCursor.KeepAnchor)
1398 1421 return cursor.selection().toPlainText()
1399 1422
1400 1423 def _get_cursor(self):
1401 1424 """ Convenience method that returns a cursor for the current position.
1402 1425 """
1403 1426 return self._control.textCursor()
1404 1427
1405 1428 def _get_end_cursor(self):
1406 1429 """ Convenience method that returns a cursor for the last character.
1407 1430 """
1408 1431 cursor = self._control.textCursor()
1409 1432 cursor.movePosition(QtGui.QTextCursor.End)
1410 1433 return cursor
1411 1434
1412 1435 def _get_input_buffer_cursor_column(self):
1413 1436 """ Returns the column of the cursor in the input buffer, excluding the
1414 1437 contribution by the prompt, or -1 if there is no such column.
1415 1438 """
1416 1439 prompt = self._get_input_buffer_cursor_prompt()
1417 1440 if prompt is None:
1418 1441 return -1
1419 1442 else:
1420 1443 cursor = self._control.textCursor()
1421 1444 return cursor.columnNumber() - len(prompt)
1422 1445
1423 1446 def _get_input_buffer_cursor_line(self):
1424 1447 """ Returns the text of the line of the input buffer that contains the
1425 1448 cursor, or None if there is no such line.
1426 1449 """
1427 1450 prompt = self._get_input_buffer_cursor_prompt()
1428 1451 if prompt is None:
1429 1452 return None
1430 1453 else:
1431 1454 cursor = self._control.textCursor()
1432 1455 text = self._get_block_plain_text(cursor.block())
1433 1456 return text[len(prompt):]
1434 1457
1435 1458 def _get_input_buffer_cursor_prompt(self):
1436 1459 """ Returns the (plain text) prompt for line of the input buffer that
1437 1460 contains the cursor, or None if there is no such line.
1438 1461 """
1439 1462 if self._executing:
1440 1463 return None
1441 1464 cursor = self._control.textCursor()
1442 1465 if cursor.position() >= self._prompt_pos:
1443 1466 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1444 1467 return self._prompt
1445 1468 else:
1446 1469 return self._continuation_prompt
1447 1470 else:
1448 1471 return None
1449 1472
1450 1473 def _get_prompt_cursor(self):
1451 1474 """ Convenience method that returns a cursor for the prompt position.
1452 1475 """
1453 1476 cursor = self._control.textCursor()
1454 1477 cursor.setPosition(self._prompt_pos)
1455 1478 return cursor
1456 1479
1457 1480 def _get_selection_cursor(self, start, end):
1458 1481 """ Convenience method that returns a cursor with text selected between
1459 1482 the positions 'start' and 'end'.
1460 1483 """
1461 1484 cursor = self._control.textCursor()
1462 1485 cursor.setPosition(start)
1463 1486 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1464 1487 return cursor
1465 1488
1466 1489 def _get_word_start_cursor(self, position):
1467 1490 """ Find the start of the word to the left the given position. If a
1468 1491 sequence of non-word characters precedes the first word, skip over
1469 1492 them. (This emulates the behavior of bash, emacs, etc.)
1470 1493 """
1471 1494 document = self._control.document()
1472 1495 position -= 1
1473 1496 while position >= self._prompt_pos and \
1474 1497 not is_letter_or_number(document.characterAt(position)):
1475 1498 position -= 1
1476 1499 while position >= self._prompt_pos and \
1477 1500 is_letter_or_number(document.characterAt(position)):
1478 1501 position -= 1
1479 1502 cursor = self._control.textCursor()
1480 1503 cursor.setPosition(position + 1)
1481 1504 return cursor
1482 1505
1483 1506 def _get_word_end_cursor(self, position):
1484 1507 """ Find the end of the word to the right the given position. If a
1485 1508 sequence of non-word characters precedes the first word, skip over
1486 1509 them. (This emulates the behavior of bash, emacs, etc.)
1487 1510 """
1488 1511 document = self._control.document()
1489 1512 end = self._get_end_cursor().position()
1490 1513 while position < end and \
1491 1514 not is_letter_or_number(document.characterAt(position)):
1492 1515 position += 1
1493 1516 while position < end and \
1494 1517 is_letter_or_number(document.characterAt(position)):
1495 1518 position += 1
1496 1519 cursor = self._control.textCursor()
1497 1520 cursor.setPosition(position)
1498 1521 return cursor
1499 1522
1500 1523 def _insert_continuation_prompt(self, cursor):
1501 1524 """ Inserts new continuation prompt using the specified cursor.
1502 1525 """
1503 1526 if self._continuation_prompt_html is None:
1504 1527 self._insert_plain_text(cursor, self._continuation_prompt)
1505 1528 else:
1506 1529 self._continuation_prompt = self._insert_html_fetching_plain_text(
1507 1530 cursor, self._continuation_prompt_html)
1508 1531
1509 1532 def _insert_html(self, cursor, html):
1510 1533 """ Inserts HTML using the specified cursor in such a way that future
1511 1534 formatting is unaffected.
1512 1535 """
1513 1536 cursor.beginEditBlock()
1514 1537 cursor.insertHtml(html)
1515 1538
1516 1539 # After inserting HTML, the text document "remembers" it's in "html
1517 1540 # mode", which means that subsequent calls adding plain text will result
1518 1541 # in unwanted formatting, lost tab characters, etc. The following code
1519 1542 # hacks around this behavior, which I consider to be a bug in Qt, by
1520 1543 # (crudely) resetting the document's style state.
1521 1544 cursor.movePosition(QtGui.QTextCursor.Left,
1522 1545 QtGui.QTextCursor.KeepAnchor)
1523 1546 if cursor.selection().toPlainText() == ' ':
1524 1547 cursor.removeSelectedText()
1525 1548 else:
1526 1549 cursor.movePosition(QtGui.QTextCursor.Right)
1527 1550 cursor.insertText(' ', QtGui.QTextCharFormat())
1528 1551 cursor.endEditBlock()
1529 1552
1530 1553 def _insert_html_fetching_plain_text(self, cursor, html):
1531 1554 """ Inserts HTML using the specified cursor, then returns its plain text
1532 1555 version.
1533 1556 """
1534 1557 cursor.beginEditBlock()
1535 1558 cursor.removeSelectedText()
1536 1559
1537 1560 start = cursor.position()
1538 1561 self._insert_html(cursor, html)
1539 1562 end = cursor.position()
1540 1563 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1541 1564 text = cursor.selection().toPlainText()
1542 1565
1543 1566 cursor.setPosition(end)
1544 1567 cursor.endEditBlock()
1545 1568 return text
1546 1569
1547 1570 def _insert_plain_text(self, cursor, text):
1548 1571 """ Inserts plain text using the specified cursor, processing ANSI codes
1549 1572 if enabled.
1550 1573 """
1551 1574 cursor.beginEditBlock()
1552 1575 if self.ansi_codes:
1553 1576 for substring in self._ansi_processor.split_string(text):
1554 1577 for act in self._ansi_processor.actions:
1555 1578
1556 1579 # Unlike real terminal emulators, we don't distinguish
1557 1580 # between the screen and the scrollback buffer. A screen
1558 1581 # erase request clears everything.
1559 1582 if act.action == 'erase' and act.area == 'screen':
1560 1583 cursor.select(QtGui.QTextCursor.Document)
1561 1584 cursor.removeSelectedText()
1562 1585
1563 1586 # Simulate a form feed by scrolling just past the last line.
1564 1587 elif act.action == 'scroll' and act.unit == 'page':
1565 1588 cursor.insertText('\n')
1566 1589 cursor.endEditBlock()
1567 1590 self._set_top_cursor(cursor)
1568 1591 cursor.joinPreviousEditBlock()
1569 1592 cursor.deletePreviousChar()
1570 1593
1571 1594 elif act.action == 'carriage-return':
1572 1595 cursor.movePosition(
1573 1596 cursor.StartOfLine, cursor.KeepAnchor)
1574 1597
1575 1598 elif act.action == 'beep':
1576 1599 QtGui.qApp.beep()
1577 1600
1578 1601 elif act.action == 'backspace':
1579 1602 if not cursor.atBlockStart():
1580 1603 cursor.movePosition(
1581 1604 cursor.PreviousCharacter, cursor.KeepAnchor)
1582 1605
1583 1606 elif act.action == 'newline':
1584 1607 cursor.movePosition(cursor.EndOfLine)
1585 1608
1586 1609 format = self._ansi_processor.get_format()
1587 1610
1588 1611 selection = cursor.selectedText()
1589 1612 if len(selection) == 0:
1590 1613 cursor.insertText(substring, format)
1591 1614 elif substring is not None:
1592 1615 # BS and CR are treated as a change in print
1593 1616 # position, rather than a backwards character
1594 1617 # deletion for output equivalence with (I)Python
1595 1618 # terminal.
1596 1619 if len(substring) >= len(selection):
1597 1620 cursor.insertText(substring, format)
1598 1621 else:
1599 1622 old_text = selection[len(substring):]
1600 1623 cursor.insertText(substring + old_text, format)
1601 1624 cursor.movePosition(cursor.PreviousCharacter,
1602 1625 cursor.KeepAnchor, len(old_text))
1603 1626 else:
1604 1627 cursor.insertText(text)
1605 1628 cursor.endEditBlock()
1606 1629
1607 1630 def _insert_plain_text_into_buffer(self, cursor, text):
1608 1631 """ Inserts text into the input buffer using the specified cursor (which
1609 1632 must be in the input buffer), ensuring that continuation prompts are
1610 1633 inserted as necessary.
1611 1634 """
1612 1635 lines = text.splitlines(True)
1613 1636 if lines:
1614 1637 cursor.beginEditBlock()
1615 1638 cursor.insertText(lines[0])
1616 1639 for line in lines[1:]:
1617 1640 if self._continuation_prompt_html is None:
1618 1641 cursor.insertText(self._continuation_prompt)
1619 1642 else:
1620 1643 self._continuation_prompt = \
1621 1644 self._insert_html_fetching_plain_text(
1622 1645 cursor, self._continuation_prompt_html)
1623 1646 cursor.insertText(line)
1624 1647 cursor.endEditBlock()
1625 1648
1626 1649 def _in_buffer(self, position=None):
1627 1650 """ Returns whether the current cursor (or, if specified, a position) is
1628 1651 inside the editing region.
1629 1652 """
1630 1653 cursor = self._control.textCursor()
1631 1654 if position is None:
1632 1655 position = cursor.position()
1633 1656 else:
1634 1657 cursor.setPosition(position)
1635 1658 line = cursor.blockNumber()
1636 1659 prompt_line = self._get_prompt_cursor().blockNumber()
1637 1660 if line == prompt_line:
1638 1661 return position >= self._prompt_pos
1639 1662 elif line > prompt_line:
1640 1663 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1641 1664 prompt_pos = cursor.position() + len(self._continuation_prompt)
1642 1665 return position >= prompt_pos
1643 1666 return False
1644 1667
1645 1668 def _keep_cursor_in_buffer(self):
1646 1669 """ Ensures that the cursor is inside the editing region. Returns
1647 1670 whether the cursor was moved.
1648 1671 """
1649 1672 moved = not self._in_buffer()
1650 1673 if moved:
1651 1674 cursor = self._control.textCursor()
1652 1675 cursor.movePosition(QtGui.QTextCursor.End)
1653 1676 self._control.setTextCursor(cursor)
1654 1677 return moved
1655 1678
1656 1679 def _keyboard_quit(self):
1657 1680 """ Cancels the current editing task ala Ctrl-G in Emacs.
1658 1681 """
1659 1682 if self._temp_buffer_filled :
1660 1683 self._cancel_completion()
1661 1684 self._clear_temporary_buffer()
1662 1685 else:
1663 1686 self.input_buffer = ''
1664 1687
1665 1688 def _page(self, text, html=False):
1666 1689 """ Displays text using the pager if it exceeds the height of the
1667 1690 viewport.
1668 1691
1669 1692 Parameters:
1670 1693 -----------
1671 1694 html : bool, optional (default False)
1672 1695 If set, the text will be interpreted as HTML instead of plain text.
1673 1696 """
1674 1697 line_height = QtGui.QFontMetrics(self.font).height()
1675 1698 minlines = self._control.viewport().height() / line_height
1676 1699 if self.paging != 'none' and \
1677 1700 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1678 1701 if self.paging == 'custom':
1679 1702 self.custom_page_requested.emit(text)
1680 1703 else:
1681 1704 self._page_control.clear()
1682 1705 cursor = self._page_control.textCursor()
1683 1706 if html:
1684 1707 self._insert_html(cursor, text)
1685 1708 else:
1686 1709 self._insert_plain_text(cursor, text)
1687 1710 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1688 1711
1689 1712 self._page_control.viewport().resize(self._control.size())
1690 1713 if self._splitter:
1691 1714 self._page_control.show()
1692 1715 self._page_control.setFocus()
1693 1716 else:
1694 1717 self.layout().setCurrentWidget(self._page_control)
1695 1718 elif html:
1696 1719 self._append_html(text)
1697 1720 else:
1698 1721 self._append_plain_text(text)
1699 1722
1700 1723 def _prompt_finished(self):
1701 1724 """ Called immediately after a prompt is finished, i.e. when some input
1702 1725 will be processed and a new prompt displayed.
1703 1726 """
1704 1727 self._control.setReadOnly(True)
1705 1728 self._prompt_finished_hook()
1706 1729
1707 1730 def _prompt_started(self):
1708 1731 """ Called immediately after a new prompt is displayed.
1709 1732 """
1710 1733 # Temporarily disable the maximum block count to permit undo/redo and
1711 1734 # to ensure that the prompt position does not change due to truncation.
1712 1735 self._control.document().setMaximumBlockCount(0)
1713 1736 self._control.setUndoRedoEnabled(True)
1714 1737
1715 1738 # Work around bug in QPlainTextEdit: input method is not re-enabled
1716 1739 # when read-only is disabled.
1717 1740 self._control.setReadOnly(False)
1718 1741 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1719 1742
1720 1743 if not self._reading:
1721 1744 self._executing = False
1722 1745 self._prompt_started_hook()
1723 1746
1724 1747 # If the input buffer has changed while executing, load it.
1725 1748 if self._input_buffer_pending:
1726 1749 self.input_buffer = self._input_buffer_pending
1727 1750 self._input_buffer_pending = ''
1728 1751
1729 1752 self._control.moveCursor(QtGui.QTextCursor.End)
1730 1753
1731 1754 def _readline(self, prompt='', callback=None):
1732 1755 """ Reads one line of input from the user.
1733 1756
1734 1757 Parameters
1735 1758 ----------
1736 1759 prompt : str, optional
1737 1760 The prompt to print before reading the line.
1738 1761
1739 1762 callback : callable, optional
1740 1763 A callback to execute with the read line. If not specified, input is
1741 1764 read *synchronously* and this method does not return until it has
1742 1765 been read.
1743 1766
1744 1767 Returns
1745 1768 -------
1746 1769 If a callback is specified, returns nothing. Otherwise, returns the
1747 1770 input string with the trailing newline stripped.
1748 1771 """
1749 1772 if self._reading:
1750 1773 raise RuntimeError('Cannot read a line. Widget is already reading.')
1751 1774
1752 1775 if not callback and not self.isVisible():
1753 1776 # If the user cannot see the widget, this function cannot return.
1754 1777 raise RuntimeError('Cannot synchronously read a line if the widget '
1755 1778 'is not visible!')
1756 1779
1757 1780 self._reading = True
1758 1781 self._show_prompt(prompt, newline=False)
1759 1782
1760 1783 if callback is None:
1761 1784 self._reading_callback = None
1762 1785 while self._reading:
1763 1786 QtCore.QCoreApplication.processEvents()
1764 1787 return self._get_input_buffer(force=True).rstrip('\n')
1765 1788
1766 1789 else:
1767 1790 self._reading_callback = lambda: \
1768 1791 callback(self._get_input_buffer(force=True).rstrip('\n'))
1769 1792
1770 1793 def _set_continuation_prompt(self, prompt, html=False):
1771 1794 """ Sets the continuation prompt.
1772 1795
1773 1796 Parameters
1774 1797 ----------
1775 1798 prompt : str
1776 1799 The prompt to show when more input is needed.
1777 1800
1778 1801 html : bool, optional (default False)
1779 1802 If set, the prompt will be inserted as formatted HTML. Otherwise,
1780 1803 the prompt will be treated as plain text, though ANSI color codes
1781 1804 will be handled.
1782 1805 """
1783 1806 if html:
1784 1807 self._continuation_prompt_html = prompt
1785 1808 else:
1786 1809 self._continuation_prompt = prompt
1787 1810 self._continuation_prompt_html = None
1788 1811
1789 1812 def _set_cursor(self, cursor):
1790 1813 """ Convenience method to set the current cursor.
1791 1814 """
1792 1815 self._control.setTextCursor(cursor)
1793 1816
1794 1817 def _set_top_cursor(self, cursor):
1795 1818 """ Scrolls the viewport so that the specified cursor is at the top.
1796 1819 """
1797 1820 scrollbar = self._control.verticalScrollBar()
1798 1821 scrollbar.setValue(scrollbar.maximum())
1799 1822 original_cursor = self._control.textCursor()
1800 1823 self._control.setTextCursor(cursor)
1801 1824 self._control.ensureCursorVisible()
1802 1825 self._control.setTextCursor(original_cursor)
1803 1826
1804 1827 def _show_prompt(self, prompt=None, html=False, newline=True):
1805 1828 """ Writes a new prompt at the end of the buffer.
1806 1829
1807 1830 Parameters
1808 1831 ----------
1809 1832 prompt : str, optional
1810 1833 The prompt to show. If not specified, the previous prompt is used.
1811 1834
1812 1835 html : bool, optional (default False)
1813 1836 Only relevant when a prompt is specified. If set, the prompt will
1814 1837 be inserted as formatted HTML. Otherwise, the prompt will be treated
1815 1838 as plain text, though ANSI color codes will be handled.
1816 1839
1817 1840 newline : bool, optional (default True)
1818 1841 If set, a new line will be written before showing the prompt if
1819 1842 there is not already a newline at the end of the buffer.
1820 1843 """
1821 1844 # Save the current end position to support _append*(before_prompt=True).
1822 1845 cursor = self._get_end_cursor()
1823 1846 self._append_before_prompt_pos = cursor.position()
1824 1847
1825 1848 # Insert a preliminary newline, if necessary.
1826 1849 if newline and cursor.position() > 0:
1827 1850 cursor.movePosition(QtGui.QTextCursor.Left,
1828 1851 QtGui.QTextCursor.KeepAnchor)
1829 1852 if cursor.selection().toPlainText() != '\n':
1830 1853 self._append_plain_text('\n')
1831 1854
1832 1855 # Write the prompt.
1833 1856 self._append_plain_text(self._prompt_sep)
1834 1857 if prompt is None:
1835 1858 if self._prompt_html is None:
1836 1859 self._append_plain_text(self._prompt)
1837 1860 else:
1838 1861 self._append_html(self._prompt_html)
1839 1862 else:
1840 1863 if html:
1841 1864 self._prompt = self._append_html_fetching_plain_text(prompt)
1842 1865 self._prompt_html = prompt
1843 1866 else:
1844 1867 self._append_plain_text(prompt)
1845 1868 self._prompt = prompt
1846 1869 self._prompt_html = None
1847 1870
1848 1871 self._prompt_pos = self._get_end_cursor().position()
1849 1872 self._prompt_started()
1850 1873
1851 1874 #------ Signal handlers ----------------------------------------------------
1852 1875
1853 1876 def _adjust_scrollbars(self):
1854 1877 """ Expands the vertical scrollbar beyond the range set by Qt.
1855 1878 """
1856 1879 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1857 1880 # and qtextedit.cpp.
1858 1881 document = self._control.document()
1859 1882 scrollbar = self._control.verticalScrollBar()
1860 1883 viewport_height = self._control.viewport().height()
1861 1884 if isinstance(self._control, QtGui.QPlainTextEdit):
1862 1885 maximum = max(0, document.lineCount() - 1)
1863 1886 step = viewport_height / self._control.fontMetrics().lineSpacing()
1864 1887 else:
1865 1888 # QTextEdit does not do line-based layout and blocks will not in
1866 1889 # general have the same height. Therefore it does not make sense to
1867 1890 # attempt to scroll in line height increments.
1868 1891 maximum = document.size().height()
1869 1892 step = viewport_height
1870 1893 diff = maximum - scrollbar.maximum()
1871 1894 scrollbar.setRange(0, maximum)
1872 1895 scrollbar.setPageStep(step)
1873 1896
1874 1897 # Compensate for undesirable scrolling that occurs automatically due to
1875 1898 # maximumBlockCount() text truncation.
1876 1899 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1877 1900 scrollbar.setValue(scrollbar.value() + diff)
1878 1901
1879 1902 def _custom_context_menu_requested(self, pos):
1880 1903 """ Shows a context menu at the given QPoint (in widget coordinates).
1881 1904 """
1882 1905 menu = self._context_menu_make(pos)
1883 1906 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now