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