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