##// END OF EJS Templates
ENH: Add continuation prompts
Gael Varoquaux -
Show More
@@ -1,337 +1,353 b''
1 1 """
2 2 Base front end class for all line-oriented frontends, rather than
3 3 block-oriented.
4 4
5 5 Currently this focuses on synchronous frontends.
6 6 """
7 7 __docformat__ = "restructuredtext en"
8 8
9 9 #-------------------------------------------------------------------------------
10 10 # Copyright (C) 2008 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-------------------------------------------------------------------------------
15 15
16 16 #-------------------------------------------------------------------------------
17 17 # Imports
18 18 #-------------------------------------------------------------------------------
19 19 import re
20 20
21 21 import sys
22 22 import codeop
23 23
24 24 from frontendbase import FrontEndBase
25 25 from IPython.kernel.core.interpreter import Interpreter
26 26
27 27 def common_prefix(strings):
28 28 """ Given a list of strings, return the common prefix between all
29 29 these strings.
30 30 """
31 31 ref = strings[0]
32 32 prefix = ''
33 33 for size in range(len(ref)):
34 34 test_prefix = ref[:size+1]
35 35 for string in strings[1:]:
36 36 if not string.startswith(test_prefix):
37 37 return prefix
38 38 prefix = test_prefix
39 39
40 40 return prefix
41 41
42 42 #-------------------------------------------------------------------------------
43 43 # Base class for the line-oriented front ends
44 44 #-------------------------------------------------------------------------------
45 45 class LineFrontEndBase(FrontEndBase):
46 46 """ Concrete implementation of the FrontEndBase class. This is meant
47 47 to be the base class behind all the frontend that are line-oriented,
48 48 rather than block-oriented.
49 49 """
50 50
51 51 # We need to keep the prompt number, to be able to increment
52 52 # it when there is an exception.
53 53 prompt_number = 1
54 54
55 55 # We keep a reference to the last result: it helps testing and
56 56 # programatic control of the frontend.
57 57 last_result = dict(number=0)
58 58
59 59 # The input buffer being edited
60 60 input_buffer = ''
61 61
62 62 # Set to true for debug output
63 63 debug = False
64 64
65 65 # A banner to print at startup
66 66 banner = None
67 67
68 68 #--------------------------------------------------------------------------
69 69 # FrontEndBase interface
70 70 #--------------------------------------------------------------------------
71 71
72 72 def __init__(self, shell=None, history=None, banner=None, *args, **kwargs):
73 73 if shell is None:
74 74 shell = Interpreter()
75 75 FrontEndBase.__init__(self, shell=shell, history=history)
76 76
77 77 if banner is not None:
78 78 self.banner = banner
79 79
80 80 def start(self):
81 81 """ Put the frontend in a state where it is ready for user
82 82 interaction.
83 83 """
84 84 if self.banner is not None:
85 85 self.write(self.banner, refresh=False)
86 86
87 87 self.new_prompt(self.input_prompt_template.substitute(number=1))
88 88
89 89
90 90 def complete(self, line):
91 91 """Complete line in engine's user_ns
92 92
93 93 Parameters
94 94 ----------
95 95 line : string
96 96
97 97 Result
98 98 ------
99 99 The replacement for the line and the list of possible completions.
100 100 """
101 101 completions = self.shell.complete(line)
102 102 complete_sep = re.compile('[\s\{\}\[\]\(\)\=]')
103 103 if completions:
104 104 prefix = common_prefix(completions)
105 105 residual = complete_sep.split(line)[:-1]
106 106 line = line[:-len(residual)] + prefix
107 107 return line, completions
108 108
109 109
110 110 def render_result(self, result):
111 111 """ Frontend-specific rendering of the result of a calculation
112 112 that has been sent to an engine.
113 113 """
114 114 if 'stdout' in result and result['stdout']:
115 115 self.write('\n' + result['stdout'])
116 116 if 'display' in result and result['display']:
117 117 self.write("%s%s\n" % (
118 118 self.output_prompt_template.substitute(
119 119 number=result['number']),
120 120 result['display']['pprint']
121 121 ) )
122 122
123 123
124 124 def render_error(self, failure):
125 125 """ Frontend-specific rendering of error.
126 126 """
127 127 self.write('\n\n'+str(failure)+'\n\n')
128 128 return failure
129 129
130 130
131 131 def is_complete(self, string):
132 132 """ Check if a string forms a complete, executable set of
133 133 commands.
134 134
135 135 For the line-oriented frontend, multi-line code is not executed
136 136 as soon as it is complete: the users has to enter two line
137 137 returns.
138 138 """
139 139 if string in ('', '\n'):
140 140 # Prefiltering, eg through ipython0, may return an empty
141 141 # string although some operations have been accomplished. We
142 142 # thus want to consider an empty string as a complete
143 143 # statement.
144 144 return True
145 145 elif ( len(self.input_buffer.split('\n'))>2
146 146 and not re.findall(r"\n[\t ]*\n[\t ]*$", string)):
147 147 return False
148 148 else:
149 149 self.capture_output()
150 150 try:
151 151 # Add line returns here, to make sure that the statement is
152 152 # complete.
153 153 is_complete = codeop.compile_command(string.rstrip() + '\n\n',
154 154 "<string>", "exec")
155 155 self.release_output()
156 156 except Exception, e:
157 157 # XXX: Hack: return True so that the
158 158 # code gets executed and the error captured.
159 159 is_complete = True
160 160 return is_complete
161 161
162 162
163 163 def write(self, string, refresh=True):
164 164 """ Write some characters to the display.
165 165
166 166 Subclass should overide this method.
167 167
168 168 The refresh keyword argument is used in frontends with an
169 169 event loop, to choose whether the write should trigget an UI
170 170 refresh, and thus be syncrhonous, or not.
171 171 """
172 172 print >>sys.__stderr__, string
173 173
174 174
175 175 def execute(self, python_string, raw_string=None):
176 176 """ Stores the raw_string in the history, and sends the
177 177 python string to the interpreter.
178 178 """
179 179 if raw_string is None:
180 180 raw_string = python_string
181 181 # Create a false result, in case there is an exception
182 182 self.last_result = dict(number=self.prompt_number)
183 183
184 184 ## try:
185 185 ## self.history.input_cache[-1] = raw_string.rstrip()
186 186 ## result = self.shell.execute(python_string)
187 187 ## self.last_result = result
188 188 ## self.render_result(result)
189 189 ## except:
190 190 ## self.show_traceback()
191 191 ## finally:
192 192 ## self.after_execute()
193 193
194 194 try:
195 195 try:
196 196 self.history.input_cache[-1] = raw_string.rstrip()
197 197 result = self.shell.execute(python_string)
198 198 self.last_result = result
199 199 self.render_result(result)
200 200 except:
201 201 self.show_traceback()
202 202 finally:
203 203 self.after_execute()
204 204
205 205
206 206 #--------------------------------------------------------------------------
207 207 # LineFrontEndBase interface
208 208 #--------------------------------------------------------------------------
209 209
210 210 def prefilter_input(self, string):
211 211 """ Prefilter the input to turn it in valid python.
212 212 """
213 213 string = string.replace('\r\n', '\n')
214 214 string = string.replace('\t', 4*' ')
215 215 # Clean the trailing whitespace
216 216 string = '\n'.join(l.rstrip() for l in string.split('\n'))
217 217 return string
218 218
219 219
220 220 def after_execute(self):
221 221 """ All the operations required after an execution to put the
222 222 terminal back in a shape where it is usable.
223 223 """
224 224 self.prompt_number += 1
225 225 self.new_prompt(self.input_prompt_template.substitute(
226 226 number=(self.last_result['number'] + 1)))
227 227 # Start a new empty history entry
228 228 self._add_history(None, '')
229 229 self.history_cursor = len(self.history.input_cache) - 1
230 230
231 231
232 232 def complete_current_input(self):
233 233 """ Do code completion on current line.
234 234 """
235 235 if self.debug:
236 236 print >>sys.__stdout__, "complete_current_input",
237 237 line = self.input_buffer
238 238 new_line, completions = self.complete(line)
239 239 if len(completions)>1:
240 240 self.write_completion(completions, new_line=new_line)
241 241 elif not line == new_line:
242 242 self.input_buffer = new_line
243 243 if self.debug:
244 244 print >>sys.__stdout__, 'line', line
245 245 print >>sys.__stdout__, 'new_line', new_line
246 246 print >>sys.__stdout__, completions
247 247
248 248
249 249 def get_line_width(self):
250 250 """ Return the width of the line in characters.
251 251 """
252 252 return 80
253 253
254 254
255 255 def write_completion(self, possibilities, new_line=None):
256 256 """ Write the list of possible completions.
257 257
258 258 new_line is the completed input line that should be displayed
259 259 after the completion are writen. If None, the input_buffer
260 260 before the completion is used.
261 261 """
262 262 if new_line is None:
263 263 new_line = self.input_buffer
264 264
265 265 self.write('\n')
266 266 max_len = len(max(possibilities, key=len)) + 1
267 267
268 268 # Now we check how much symbol we can put on a line...
269 269 chars_per_line = self.get_line_width()
270 270 symbols_per_line = max(1, chars_per_line/max_len)
271 271
272 272 pos = 1
273 273 buf = []
274 274 for symbol in possibilities:
275 275 if pos < symbols_per_line:
276 276 buf.append(symbol.ljust(max_len))
277 277 pos += 1
278 278 else:
279 279 buf.append(symbol.rstrip() + '\n')
280 280 pos = 1
281 281 self.write(''.join(buf))
282 282 self.new_prompt(self.input_prompt_template.substitute(
283 283 number=self.last_result['number'] + 1))
284 284 self.input_buffer = new_line
285 285
286 286
287 287 def new_prompt(self, prompt):
288 288 """ Prints a prompt and starts a new editing buffer.
289 289
290 290 Subclasses should use this method to make sure that the
291 291 terminal is put in a state favorable for a new line
292 292 input.
293 293 """
294 294 self.input_buffer = ''
295 295 self.write(prompt)
296 296
297 297
298 def continuation_prompt(self):
299 """Returns the current continuation prompt.
300 Overridden to generate a continuation prompt matching the length of the
301 current prompt."""
302
303 # FIXME: This is a bad hack.. I need to find a way to use the 'Prompt2'
304 # class in IPython/kernel/prompts.py. Basically, I am trying to get the
305 # length of the current prompt ("In ['number']").
306 return ("."*(5+len(str(self.last_result['number']))) + ':')
307
308
298 309 def execute_command(self, command, hidden=False):
299 310 """ Execute a command, not only in the model, but also in the
300 311 view, if any.
301 312 """
302 313 return self.shell.execute(command)
303 314
304 315 #--------------------------------------------------------------------------
305 316 # Private API
306 317 #--------------------------------------------------------------------------
307 318
308 319 def _on_enter(self):
309 320 """ Called when the return key is pressed in a line editing
310 321 buffer.
311 322 """
312 323 current_buffer = self.input_buffer
313 cleaned_buffer = self.prefilter_input(current_buffer)
324 # XXX: This string replace is ugly, but there should be no way it
325 # fails.
326 prompt_less_buffer = re.sub('^' + self.continuation_prompt(),
327 '', current_buffer).replace('\n' + self.continuation_prompt(),
328 '\n')
329 cleaned_buffer = self.prefilter_input(prompt_less_buffer)
314 330 if self.is_complete(cleaned_buffer):
315 331 self.execute(cleaned_buffer, raw_string=current_buffer)
316 332 else:
317 self.input_buffer += self._get_indent_string(
318 current_buffer[:-1])
333 self.input_buffer += self.continuation_prompt() + \
334 self._get_indent_string(prompt_less_buffer[:-1])
319 335 if len(current_buffer.split('\n')) == 2:
320 self.input_buffer += '\t\t'
336 self.input_buffer += '\t'
321 337 if current_buffer[:-1].split('\n')[-1].rstrip().endswith(':'):
322 338 self.input_buffer += '\t'
323 339
324 340
325 341 def _get_indent_string(self, string):
326 342 """ Return the string of whitespace that prefixes a line. Used to
327 343 add the right amount of indendation when creating a new line.
328 344 """
329 345 string = string.replace('\t', ' '*4)
330 346 string = string.split('\n')[-1]
331 347 indent_chars = len(string) - len(string.lstrip())
332 348 indent_string = '\t'*(indent_chars // 4) + \
333 349 ' '*(indent_chars % 4)
334 350
335 351 return indent_string
336 352
337 353
@@ -1,257 +1,275 b''
1 1 """
2 2 Frontend class that uses IPython0 to prefilter the inputs.
3 3
4 4 Using the IPython0 mechanism gives us access to the magics.
5 5
6 6 This is a transitory class, used here to do the transition between
7 7 ipython0 and ipython1. This class is meant to be short-lived as more
8 8 functionnality is abstracted out of ipython0 in reusable functions and
9 9 is added on the interpreter. This class can be a used to guide this
10 10 refactoring.
11 11 """
12 12 __docformat__ = "restructuredtext en"
13 13
14 14 #-------------------------------------------------------------------------------
15 15 # Copyright (C) 2008 The IPython Development Team
16 16 #
17 17 # Distributed under the terms of the BSD License. The full license is in
18 18 # the file COPYING, distributed as part of this software.
19 19 #-------------------------------------------------------------------------------
20 20
21 21 #-------------------------------------------------------------------------------
22 22 # Imports
23 23 #-------------------------------------------------------------------------------
24 24 import sys
25 25 import pydoc
26 26 import os
27 import re
27 28 import __builtin__
28 29
29 30 from IPython.ipmaker import make_IPython
30 31 from IPython.ipapi import IPApi
31 32 from IPython.kernel.core.redirector_output_trap import RedirectorOutputTrap
32 33
33 34 from IPython.kernel.core.sync_traceback_trap import SyncTracebackTrap
34 35
35 36 from IPython.genutils import Term
36 37
37 38 from linefrontendbase import LineFrontEndBase, common_prefix
38 39
39 40
40 41 def mk_system_call(system_call_function, command):
41 42 """ given a os.system replacement, and a leading string command,
42 43 returns a function that will execute the command with the given
43 44 argument string.
44 45 """
45 46 def my_system_call(args):
46 47 system_call_function("%s %s" % (command, args))
47 48
48 49 my_system_call.__doc__ = "Calls %s" % command
49 50 return my_system_call
50 51
51 52 #-------------------------------------------------------------------------------
52 53 # Frontend class using ipython0 to do the prefiltering.
53 54 #-------------------------------------------------------------------------------
54 55 class PrefilterFrontEnd(LineFrontEndBase):
55 56 """ Class that uses ipython0 to do prefilter the input, do the
56 57 completion and the magics.
57 58
58 59 The core trick is to use an ipython0 instance to prefilter the
59 60 input, and share the namespace between the interpreter instance used
60 61 to execute the statements and the ipython0 used for code
61 62 completion...
62 63 """
63 64
64 65 debug = False
65 66
66 67 def __init__(self, ipython0=None, *args, **kwargs):
67 68 """ Parameters:
68 69 -----------
69 70
70 71 ipython0: an optional ipython0 instance to use for command
71 72 prefiltering and completion.
72 73 """
73 74 LineFrontEndBase.__init__(self, *args, **kwargs)
74 75 self.shell.output_trap = RedirectorOutputTrap(
75 76 out_callback=self.write,
76 77 err_callback=self.write,
77 78 )
78 79 self.shell.traceback_trap = SyncTracebackTrap(
79 80 formatters=self.shell.traceback_trap.formatters,
80 81 )
81 82
82 83 # Start the ipython0 instance:
83 84 self.save_output_hooks()
84 85 if ipython0 is None:
85 86 # Instanciate an IPython0 interpreter to be able to use the
86 87 # prefiltering.
87 88 # Suppress all key input, to avoid waiting
88 89 def my_rawinput(x=None):
89 90 return '\n'
90 91 old_rawinput = __builtin__.raw_input
91 92 __builtin__.raw_input = my_rawinput
92 93 # XXX: argv=[] is a bit bold.
93 94 ipython0 = make_IPython(argv=[],
94 95 user_ns=self.shell.user_ns,
95 96 user_global_ns=self.shell.user_global_ns)
96 97 __builtin__.raw_input = old_rawinput
97 98 self.ipython0 = ipython0
98 99 # Set the pager:
99 100 self.ipython0.set_hook('show_in_pager',
100 101 lambda s, string: self.write("\n" + string))
101 102 self.ipython0.write = self.write
102 103 self._ip = _ip = IPApi(self.ipython0)
103 104 # Make sure the raw system call doesn't get called, as we don't
104 105 # have a stdin accessible.
105 106 self._ip.system = self.system_call
106 107 # XXX: Muck around with magics so that they work better
107 108 # in our environment
108 109 if not sys.platform.startswith('win'):
109 110 self.ipython0.magic_ls = mk_system_call(self.system_call,
110 111 'ls -CF')
111 112 # And now clean up the mess created by ipython0
112 113 self.release_output()
113 114
114 115
115 116 if not 'banner' in kwargs and self.banner is None:
116 self.banner = self.ipython0.BANNER + """
117 This is the wx frontend, by Gael Varoquaux. This is EXPERIMENTAL code."""
117 self.banner = self.ipython0.BANNER
118 118
119 # FIXME: __init__ and start should be two different steps
119 120 self.start()
120 121
121 122 #--------------------------------------------------------------------------
122 123 # FrontEndBase interface
123 124 #--------------------------------------------------------------------------
124 125
125 126 def show_traceback(self):
126 127 """ Use ipython0 to capture the last traceback and display it.
127 128 """
128 129 # Don't do the capture; the except_hook has already done some
129 130 # modifications to the IO streams, if we store them, we'll be
130 131 # storing the wrong ones.
131 132 #self.capture_output()
132 133 self.ipython0.showtraceback(tb_offset=-1)
133 134 self.release_output()
134 135
135 136
136 137 def execute(self, python_string, raw_string=None):
137 138 if self.debug:
138 139 print 'Executing Python code:', repr(python_string)
139 140 self.capture_output()
140 141 LineFrontEndBase.execute(self, python_string,
141 142 raw_string=raw_string)
142 143 self.release_output()
143 144
144 145
145 146 def save_output_hooks(self):
146 147 """ Store all the output hooks we can think of, to be able to
147 148 restore them.
148 149
149 150 We need to do this early, as starting the ipython0 instance will
150 151 screw ouput hooks.
151 152 """
152 153 self.__old_cout_write = Term.cout.write
153 154 self.__old_cerr_write = Term.cerr.write
154 155 self.__old_stdout = sys.stdout
155 156 self.__old_stderr= sys.stderr
156 157 self.__old_help_output = pydoc.help.output
157 158 self.__old_display_hook = sys.displayhook
158 159
159 160
160 161 def capture_output(self):
161 162 """ Capture all the output mechanisms we can think of.
162 163 """
163 164 self.save_output_hooks()
164 165 Term.cout.write = self.write
165 166 Term.cerr.write = self.write
166 167 sys.stdout = Term.cout
167 168 sys.stderr = Term.cerr
168 169 pydoc.help.output = self.shell.output_trap.out
169 170
170 171
171 172 def release_output(self):
172 173 """ Release all the different captures we have made.
173 174 """
174 175 Term.cout.write = self.__old_cout_write
175 176 Term.cerr.write = self.__old_cerr_write
176 177 sys.stdout = self.__old_stdout
177 178 sys.stderr = self.__old_stderr
178 179 pydoc.help.output = self.__old_help_output
179 180 sys.displayhook = self.__old_display_hook
180 181
181 182
182 183 def complete(self, line):
183 184 # FIXME: This should be factored out in the linefrontendbase
184 185 # method.
186 word = self._get_completion_text(line)
185 187 word = line.split('\n')[-1].split(' ')[-1]
188 print 'Completion', word
186 189 completions = self.ipython0.complete(word)
187 190 # FIXME: The proper sort should be done in the complete method.
188 191 key = lambda x: x.replace('_', '')
189 192 completions.sort(key=key)
190 193 if completions:
191 194 prefix = common_prefix(completions)
192 195 line = line[:-len(word)] + prefix
193 196 return line, completions
194 197
195 198
196 199 #--------------------------------------------------------------------------
197 200 # LineFrontEndBase interface
198 201 #--------------------------------------------------------------------------
199 202
200 203 def prefilter_input(self, input_string):
201 204 """ Using IPython0 to prefilter the commands to turn them
202 205 in executable statements that are valid Python strings.
203 206 """
204 207 input_string = LineFrontEndBase.prefilter_input(self, input_string)
205 208 filtered_lines = []
206 209 # The IPython0 prefilters sometime produce output. We need to
207 210 # capture it.
208 211 self.capture_output()
209 212 self.last_result = dict(number=self.prompt_number)
210 213
211 214 ## try:
212 215 ## for line in input_string.split('\n'):
213 216 ## filtered_lines.append(
214 217 ## self.ipython0.prefilter(line, False).rstrip())
215 218 ## except:
216 219 ## # XXX: probably not the right thing to do.
217 220 ## self.ipython0.showsyntaxerror()
218 221 ## self.after_execute()
219 222 ## finally:
220 223 ## self.release_output()
221 224
222 225
223 226 try:
224 227 try:
225 228 for line in input_string.split('\n'):
226 229 filtered_lines.append(
227 230 self.ipython0.prefilter(line, False).rstrip())
228 231 except:
229 232 # XXX: probably not the right thing to do.
230 233 self.ipython0.showsyntaxerror()
231 234 self.after_execute()
232 235 finally:
233 236 self.release_output()
234 237
235 238
236 239
237 240 # Clean up the trailing whitespace, to avoid indentation errors
238 241 filtered_string = '\n'.join(filtered_lines)
239 242 return filtered_string
240 243
241 244
242 245 #--------------------------------------------------------------------------
243 246 # PrefilterFrontEnd interface
244 247 #--------------------------------------------------------------------------
245 248
246 249 def system_call(self, command_string):
247 250 """ Allows for frontend to define their own system call, to be
248 251 able capture output and redirect input.
249 252 """
250 253 return os.system(command_string)
251 254
252 255
253 256 def do_exit(self):
254 257 """ Exit the shell, cleanup and save the history.
255 258 """
256 259 self.ipython0.atexit_operations()
257 260
261
262 def _get_completion_text(self, line):
263 """ Returns the text to be completed by breaking the line at specified
264 delimiters.
265 """
266 # Break at: spaces, '=', all parentheses (except if balanced).
267 # FIXME2: In the future, we need to make the implementation similar to
268 # that in the 'pyreadline' module (modes/basemode.py) where we break at
269 # each delimiter and try to complete the residual line, until we get a
270 # successful list of completions.
271 expression = '\s|=|,|:|\((?!.*\))|\[(?!.*\])|\{(?!.*\})'
272 complete_sep = re.compile(expression)
273 text = complete_sep.split(line)[-1]
274 return text
275
@@ -1,436 +1,482 b''
1 1 # encoding: utf-8
2 2 """
3 3 A Wx widget to act as a console and input commands.
4 4
5 5 This widget deals with prompts and provides an edit buffer
6 6 restricted to after the last prompt.
7 7 """
8 8
9 9 __docformat__ = "restructuredtext en"
10 10
11 11 #-------------------------------------------------------------------------------
12 12 # Copyright (C) 2008 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is
15 15 # in the file COPYING, distributed as part of this software.
16 16 #-------------------------------------------------------------------------------
17 17
18 18 #-------------------------------------------------------------------------------
19 19 # Imports
20 20 #-------------------------------------------------------------------------------
21 21
22 22 import wx
23 23 import wx.stc as stc
24 24
25 25 from wx.py import editwindow
26 26 import time
27 27 import sys
28 28 LINESEP = '\n'
29 29 if sys.platform == 'win32':
30 30 LINESEP = '\n\r'
31 31
32 32 import re
33 33
34 34 # FIXME: Need to provide an API for non user-generated display on the
35 35 # screen: this should not be editable by the user.
36 36
37 37 _DEFAULT_SIZE = 10
38 38 if sys.platform == 'darwin':
39 39 _DEFAULT_SIZE = 12
40 40
41 41 _DEFAULT_STYLE = {
42 42 'stdout' : 'fore:#0000FF',
43 43 'stderr' : 'fore:#007f00',
44 44 'trace' : 'fore:#FF0000',
45 45
46 46 'default' : 'size:%d' % _DEFAULT_SIZE,
47 47 'bracegood' : 'fore:#00AA00,back:#000000,bold',
48 48 'bracebad' : 'fore:#FF0000,back:#000000,bold',
49 49
50 50 # properties for the various Python lexer styles
51 51 'comment' : 'fore:#007F00',
52 52 'number' : 'fore:#007F7F',
53 53 'string' : 'fore:#7F007F,italic',
54 54 'char' : 'fore:#7F007F,italic',
55 55 'keyword' : 'fore:#00007F,bold',
56 56 'triple' : 'fore:#7F0000',
57 57 'tripledouble' : 'fore:#7F0000',
58 58 'class' : 'fore:#0000FF,bold,underline',
59 59 'def' : 'fore:#007F7F,bold',
60 60 'operator' : 'bold'
61 61 }
62 62
63 63 # new style numbers
64 64 _STDOUT_STYLE = 15
65 65 _STDERR_STYLE = 16
66 66 _TRACE_STYLE = 17
67 67
68 68
69 69 # system colors
70 70 #SYS_COLOUR_BACKGROUND = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)
71 71
72 72 #-------------------------------------------------------------------------------
73 73 # The console widget class
74 74 #-------------------------------------------------------------------------------
75 75 class ConsoleWidget(editwindow.EditWindow):
76 76 """ Specialized styled text control view for console-like workflow.
77 77
78 78 This widget is mainly interested in dealing with the prompt and
79 79 keeping the cursor inside the editing line.
80 80 """
81 81
82 82 # This is where the title captured from the ANSI escape sequences are
83 83 # stored.
84 84 title = 'Console'
85 85
86 86 # The buffer being edited.
87 87 def _set_input_buffer(self, string):
88 88 self.SetSelection(self.current_prompt_pos, self.GetLength())
89 89 self.ReplaceSelection(string)
90 90 self.GotoPos(self.GetLength())
91 91
92 92 def _get_input_buffer(self):
93 93 """ Returns the text in current edit buffer.
94 94 """
95 95 input_buffer = self.GetTextRange(self.current_prompt_pos,
96 96 self.GetLength())
97 97 input_buffer = input_buffer.replace(LINESEP, '\n')
98 98 return input_buffer
99 99
100 100 input_buffer = property(_get_input_buffer, _set_input_buffer)
101 101
102 102 style = _DEFAULT_STYLE.copy()
103 103
104 104 # Translation table from ANSI escape sequences to color. Override
105 105 # this to specify your colors.
106 106 ANSI_STYLES = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'],
107 107 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
108 108 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
109 109 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
110 110 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
111 111 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
112 112 '1;34': [12, 'LIGHT BLUE'], '1;35':
113 113 [13, 'MEDIUM VIOLET RED'],
114 114 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
115 115
116 116 # The color of the carret (call _apply_style() after setting)
117 117 carret_color = 'BLACK'
118 118
119 119 # Store the last time a refresh was done
120 120 _last_refresh_time = 0
121 121
122 122 #--------------------------------------------------------------------------
123 123 # Public API
124 124 #--------------------------------------------------------------------------
125 125
126 126 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
127 127 size=wx.DefaultSize, style=wx.WANTS_CHARS, ):
128 128 editwindow.EditWindow.__init__(self, parent, id, pos, size, style)
129 129 self._configure_scintilla()
130 130
131 131 self.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
132 132 self.Bind(wx.EVT_KEY_UP, self._on_key_up)
133 133
134 134
135 135 def write(self, text, refresh=True):
136 136 """ Write given text to buffer, while translating the ansi escape
137 137 sequences.
138 138 """
139 139 # XXX: do not put print statements to sys.stdout/sys.stderr in
140 140 # this method, the print statements will call this method, as
141 141 # you will end up with an infinit loop
142 142 title = self.title_pat.split(text)
143 143 if len(title)>1:
144 144 self.title = title[-2]
145 145
146 146 text = self.title_pat.sub('', text)
147 147 segments = self.color_pat.split(text)
148 148 segment = segments.pop(0)
149 149 self.GotoPos(self.GetLength())
150 150 self.StartStyling(self.GetLength(), 0xFF)
151 151 try:
152 152 self.AppendText(segment)
153 153 except UnicodeDecodeError:
154 154 # XXX: Do I really want to skip the exception?
155 155 pass
156 156
157 157 if segments:
158 158 for ansi_tag, text in zip(segments[::2], segments[1::2]):
159 159 self.StartStyling(self.GetLength(), 0xFF)
160 160 try:
161 161 self.AppendText(text)
162 162 except UnicodeDecodeError:
163 163 # XXX: Do I really want to skip the exception?
164 164 pass
165 165
166 166 if ansi_tag not in self.ANSI_STYLES:
167 167 style = 0
168 168 else:
169 169 style = self.ANSI_STYLES[ansi_tag][0]
170 170
171 171 self.SetStyling(len(text), style)
172 172
173 173 self.GotoPos(self.GetLength())
174 174 if refresh:
175 175 current_time = time.time()
176 176 if current_time - self._last_refresh_time > 0.03:
177 177 if sys.platform == 'win32':
178 178 wx.SafeYield()
179 179 else:
180 180 wx.Yield()
181 181 # self.ProcessEvent(wx.PaintEvent())
182 182 self._last_refresh_time = current_time
183 183
184 184
185 185 def new_prompt(self, prompt):
186 186 """ Prints a prompt at start of line, and move the start of the
187 187 current block there.
188 188
189 189 The prompt can be given with ascii escape sequences.
190 190 """
191 191 self.write(prompt, refresh=False)
192 192 # now we update our cursor giving end of prompt
193 193 self.current_prompt_pos = self.GetLength()
194 194 self.current_prompt_line = self.GetCurrentLine()
195 195 self.EnsureCaretVisible()
196 196
197 197
198 198 def scroll_to_bottom(self):
199 199 maxrange = self.GetScrollRange(wx.VERTICAL)
200 200 self.ScrollLines(maxrange)
201 201
202 202
203 203 def pop_completion(self, possibilities, offset=0):
204 204 """ Pops up an autocompletion menu. Offset is the offset
205 205 in characters of the position at which the menu should
206 206 appear, relativ to the cursor.
207 207 """
208 208 self.AutoCompSetIgnoreCase(False)
209 209 self.AutoCompSetAutoHide(False)
210 210 self.AutoCompSetMaxHeight(len(possibilities))
211 211 self.AutoCompShow(offset, " ".join(possibilities))
212 212
213 213
214 214 def get_line_width(self):
215 215 """ Return the width of the line in characters.
216 216 """
217 217 return self.GetSize()[0]/self.GetCharWidth()
218 218
219
220 def clear_screen(self):
221 """ Empty completely the widget.
222 """
223 self.ClearAll()
224 self.new_prompt(self.input_prompt_template.substitute(
225 number=(self.last_result['number'] + 1)))
226
227
228
219 229 #--------------------------------------------------------------------------
220 230 # EditWindow API
221 231 #--------------------------------------------------------------------------
222 232
223 233 def OnUpdateUI(self, event):
224 234 """ Override the OnUpdateUI of the EditWindow class, to prevent
225 235 syntax highlighting both for faster redraw, and for more
226 236 consistent look and feel.
227 237 """
228 238
229 239 #--------------------------------------------------------------------------
230 240 # Private API
231 241 #--------------------------------------------------------------------------
232 242
233 243 def _apply_style(self):
234 244 """ Applies the colors for the different text elements and the
235 245 carret.
236 246 """
237 247 self.SetCaretForeground(self.carret_color)
238 248
239 249 #self.StyleClearAll()
240 250 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,
241 251 "fore:#FF0000,back:#0000FF,bold")
242 252 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,
243 253 "fore:#000000,back:#FF0000,bold")
244 254
245 255 for style in self.ANSI_STYLES.values():
246 256 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
247 257
248 258
249 259 def _configure_scintilla(self):
250 260 self.SetEOLMode(stc.STC_EOL_LF)
251 261
252 262 # Ctrl"+" or Ctrl "-" can be used to zoomin/zoomout the text inside
253 263 # the widget
254 264 self.CmdKeyAssign(ord('+'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
255 265 self.CmdKeyAssign(ord('-'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
256 266 # Also allow Ctrl Shift "=" for poor non US keyboard users.
257 267 self.CmdKeyAssign(ord('='), stc.STC_SCMOD_CTRL|stc.STC_SCMOD_SHIFT,
258 268 stc.STC_CMD_ZOOMIN)
259 269
260 270 # Keys: we need to clear some of the keys the that don't play
261 271 # well with a console.
262 272 self.CmdKeyClear(ord('D'), stc.STC_SCMOD_CTRL)
263 273 self.CmdKeyClear(ord('L'), stc.STC_SCMOD_CTRL)
264 274 self.CmdKeyClear(ord('T'), stc.STC_SCMOD_CTRL)
265 275 self.CmdKeyClear(ord('A'), stc.STC_SCMOD_CTRL)
266 276
267 277 self.SetEOLMode(stc.STC_EOL_CRLF)
268 278 self.SetWrapMode(stc.STC_WRAP_CHAR)
269 279 self.SetWrapMode(stc.STC_WRAP_WORD)
270 280 self.SetBufferedDraw(True)
271 281 self.SetUseAntiAliasing(True)
272 282 self.SetLayoutCache(stc.STC_CACHE_PAGE)
273 283 self.SetUndoCollection(False)
274 284 self.SetUseTabs(True)
275 285 self.SetIndent(4)
276 286 self.SetTabWidth(4)
277 287
278 288 # we don't want scintilla's autocompletion to choose
279 289 # automaticaly out of a single choice list, as we pop it up
280 290 # automaticaly
281 291 self.AutoCompSetChooseSingle(False)
282 292 self.AutoCompSetMaxHeight(10)
283 293 # XXX: this doesn't seem to have an effect.
284 294 self.AutoCompSetFillUps('\n')
285 295
286 296 self.SetMargins(3, 3) #text is moved away from border with 3px
287 297 # Suppressing Scintilla margins
288 298 self.SetMarginWidth(0, 0)
289 299 self.SetMarginWidth(1, 0)
290 300 self.SetMarginWidth(2, 0)
291 301
292 302 self._apply_style()
293 303
294 304 # Xterm escape sequences
295 305 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
296 306 self.title_pat = re.compile('\x1b]0;(.*?)\x07')
297 307
298 308 #self.SetEdgeMode(stc.STC_EDGE_LINE)
299 309 #self.SetEdgeColumn(80)
300 310
301 311 # styles
302 312 p = self.style
303 313 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default'])
304 314 self.StyleClearAll()
305 315 self.StyleSetSpec(_STDOUT_STYLE, p['stdout'])
306 316 self.StyleSetSpec(_STDERR_STYLE, p['stderr'])
307 317 self.StyleSetSpec(_TRACE_STYLE, p['trace'])
308 318
309 319 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, p['bracegood'])
310 320 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, p['bracebad'])
311 321 self.StyleSetSpec(stc.STC_P_COMMENTLINE, p['comment'])
312 322 self.StyleSetSpec(stc.STC_P_NUMBER, p['number'])
313 323 self.StyleSetSpec(stc.STC_P_STRING, p['string'])
314 324 self.StyleSetSpec(stc.STC_P_CHARACTER, p['char'])
315 325 self.StyleSetSpec(stc.STC_P_WORD, p['keyword'])
316 326 self.StyleSetSpec(stc.STC_P_WORD2, p['keyword'])
317 327 self.StyleSetSpec(stc.STC_P_TRIPLE, p['triple'])
318 328 self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, p['tripledouble'])
319 329 self.StyleSetSpec(stc.STC_P_CLASSNAME, p['class'])
320 330 self.StyleSetSpec(stc.STC_P_DEFNAME, p['def'])
321 331 self.StyleSetSpec(stc.STC_P_OPERATOR, p['operator'])
322 332 self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, p['comment'])
323 333
324 334 def _on_key_down(self, event, skip=True):
325 335 """ Key press callback used for correcting behavior for
326 336 console-like interfaces: the cursor is constraint to be after
327 337 the last prompt.
328 338
329 339 Return True if event as been catched.
330 340 """
331 341 catched = True
332 342 # Intercept some specific keys.
333 343 if event.KeyCode == ord('L') and event.ControlDown() :
334 344 self.scroll_to_bottom()
335 345 elif event.KeyCode == ord('K') and event.ControlDown() :
336 346 self.input_buffer = ''
337 347 elif event.KeyCode == ord('A') and event.ControlDown() :
338 348 self.GotoPos(self.GetLength())
339 349 self.SetSelectionStart(self.current_prompt_pos)
340 350 self.SetSelectionEnd(self.GetCurrentPos())
341 351 catched = True
342 352 elif event.KeyCode == ord('E') and event.ControlDown() :
343 353 self.GotoPos(self.GetLength())
344 354 catched = True
345 355 elif event.KeyCode == wx.WXK_PAGEUP:
346 356 self.ScrollPages(-1)
347 357 elif event.KeyCode == wx.WXK_PAGEDOWN:
348 358 self.ScrollPages(1)
349 359 elif event.KeyCode == wx.WXK_UP and event.ShiftDown():
350 360 self.ScrollLines(-1)
351 361 elif event.KeyCode == wx.WXK_DOWN and event.ShiftDown():
352 362 self.ScrollLines(1)
353 363 else:
354 364 catched = False
355 365
356 366 if self.AutoCompActive():
357 367 event.Skip()
358 368 else:
359 369 if event.KeyCode in (13, wx.WXK_NUMPAD_ENTER) and \
360 370 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
361 371 catched = True
362 372 self.CallTipCancel()
363 373 self.write('\n', refresh=False)
364 374 # Under windows scintilla seems to be doing funny stuff to the
365 375 # line returns here, but the getter for input_buffer filters
366 376 # this out.
367 377 if sys.platform == 'win32':
368 378 self.input_buffer = self.input_buffer
369 379 self._on_enter()
370 380
371 381 elif event.KeyCode == wx.WXK_HOME:
372 382 if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
373 383 self.GotoPos(self.current_prompt_pos)
374 384 catched = True
375 385
376 386 elif event.Modifiers == wx.MOD_SHIFT:
377 387 # FIXME: This behavior is not ideal: if the selection
378 388 # is already started, it will jump.
379 389 self.SetSelectionStart(self.current_prompt_pos)
380 390 self.SetSelectionEnd(self.GetCurrentPos())
381 391 catched = True
382 392
383 393 elif event.KeyCode == wx.WXK_UP:
384 394 if self.GetCurrentLine() > self.current_prompt_line:
385 395 if self.GetCurrentLine() == self.current_prompt_line + 1 \
386 396 and self.GetColumn(self.GetCurrentPos()) < \
387 397 self.GetColumn(self.current_prompt_pos):
388 398 self.GotoPos(self.current_prompt_pos)
389 399 else:
390 400 event.Skip()
391 401 catched = True
392 402
393 403 elif event.KeyCode in (wx.WXK_LEFT, wx.WXK_BACK):
394 if self.GetCurrentPos() > self.current_prompt_pos:
404 if not self._keep_cursor_in_buffer():
395 405 event.Skip()
396 406 catched = True
397 407
398 408 if skip and not catched:
399 409 # Put the cursor back in the edit region
400 if self.GetCurrentPos() < self.current_prompt_pos:
401 self.GotoPos(self.current_prompt_pos)
402 else:
410 if not self._keep_cursor_in_buffer():
411 if (self.GetCurrentPos() == self.GetLength()
412 and event.KeyCode == wx.WXK_DELETE):
413 pass
403 414 event.Skip()
404 415
405 416 return catched
406 417
407 418
408 419 def _on_key_up(self, event, skip=True):
409 420 """ If cursor is outside the editing region, put it back.
410 421 """
411 event.Skip()
412 if self.GetCurrentPos() < self.current_prompt_pos:
422 if skip:
423 event.Skip()
424 self._keep_cursor_in_buffer()
425
426
427 def _keep_cursor_in_buffer(self):
428 """ Checks if the cursor is where it is allowed to be. If not,
429 put it back.
430
431 Returns
432 -------
433 cursor_moved: Boolean
434 whether or not the cursor was moved by this routine.
435
436 Notes
437 ------
438 WARNING: This does proper checks only for horizontal
439 movements.
440 """
441 current_pos = self.GetCurrentPos()
442 if current_pos < self.current_prompt_pos:
413 443 self.GotoPos(self.current_prompt_pos)
444 return True
445 line, line_pos = self.GetCurLine()
446 # Jump the continuation prompt
447 continuation_prompt = self.continuation_prompt()
448 if ( line.startswith(continuation_prompt)
449 and line_pos < len(continuation_prompt)+1):
450 if line_pos < 2:
451 # We are at the beginning of the line, trying to move
452 # forward: jump forward.
453 self.GotoPos(current_pos + 1 +
454 len(continuation_prompt) - line_pos)
455 else:
456 # Jump back up
457 self.GotoPos(self.GetLineEndPosition(self.GetCurrentLine()-1))
458 return True
459 return False
414 460
415 461
416 462
417 463 if __name__ == '__main__':
418 464 # Some simple code to test the console widget.
419 465 class MainWindow(wx.Frame):
420 466 def __init__(self, parent, id, title):
421 467 wx.Frame.__init__(self, parent, id, title, size=(300,250))
422 468 self._sizer = wx.BoxSizer(wx.VERTICAL)
423 469 self.console_widget = ConsoleWidget(self)
424 470 self._sizer.Add(self.console_widget, 1, wx.EXPAND)
425 471 self.SetSizer(self._sizer)
426 472 self.SetAutoLayout(1)
427 473 self.Show(True)
428 474
429 475 app = wx.PySimpleApp()
430 476 w = MainWindow(None, wx.ID_ANY, 'ConsoleWidget')
431 477 w.SetSize((780, 460))
432 478 w.Show()
433 479
434 480 app.MainLoop()
435 481
436 482
@@ -1,558 +1,564 b''
1 1 # encoding: utf-8 -*- test-case-name:
2 2 # FIXME: Need to add tests.
3 3 # ipython1.frontend.wx.tests.test_wx_frontend -*-
4 4
5 5 """Classes to provide a Wx frontend to the
6 6 IPython.kernel.core.interpreter.
7 7
8 8 This class inherits from ConsoleWidget, that provides a console-like
9 9 widget to provide a text-rendering widget suitable for a terminal.
10 10 """
11 11
12 12 __docformat__ = "restructuredtext en"
13 13
14 14 #-------------------------------------------------------------------------------
15 15 # Copyright (C) 2008 The IPython Development Team
16 16 #
17 17 # Distributed under the terms of the BSD License. The full license is in
18 18 # the file COPYING, distributed as part of this software.
19 19 #-------------------------------------------------------------------------------
20 20
21 21 #-------------------------------------------------------------------------------
22 22 # Imports
23 23 #-------------------------------------------------------------------------------
24 24
25 25 # Major library imports
26 26 import re
27 27 import __builtin__
28 28 import sys
29 29 from threading import Lock
30 30 import string
31 31
32 32 import wx
33 33 from wx import stc
34 34
35 35 # Ipython-specific imports.
36 36 from IPython.frontend._process import PipedProcess
37 37 from console_widget import ConsoleWidget
38 38 from IPython.frontend.prefilterfrontend import PrefilterFrontEnd
39 39
40 40 #-------------------------------------------------------------------------------
41 41 # Constants
42 42 #-------------------------------------------------------------------------------
43 43
44 44 _COMPLETE_BUFFER_BG = '#FAFAF1' # Nice green
45 45 _INPUT_BUFFER_BG = '#FDFFD3' # Nice yellow
46 46 _ERROR_BG = '#FFF1F1' # Nice red
47 47
48 48 _COMPLETE_BUFFER_MARKER = 31
49 49 _ERROR_MARKER = 30
50 50 _INPUT_MARKER = 29
51 51
52 52 prompt_in1 = \
53 53 '\n\x01\x1b[0;34m\x02In [\x01\x1b[1;34m\x02$number\x01\x1b[0;34m\x02]: \x01\x1b[0m\x02'
54 54
55 55 prompt_out = \
56 56 '\x01\x1b[0;31m\x02Out[\x01\x1b[1;31m\x02$number\x01\x1b[0;31m\x02]: \x01\x1b[0m\x02'
57 57
58 58 #-------------------------------------------------------------------------------
59 59 # Classes to implement the Wx frontend
60 60 #-------------------------------------------------------------------------------
61 61 class WxController(ConsoleWidget, PrefilterFrontEnd):
62 62 """Classes to provide a Wx frontend to the
63 63 IPython.kernel.core.interpreter.
64 64
65 65 This class inherits from ConsoleWidget, that provides a console-like
66 66 widget to provide a text-rendering widget suitable for a terminal.
67 67 """
68 68
69 69 output_prompt_template = string.Template(prompt_out)
70 70
71 71 input_prompt_template = string.Template(prompt_in1)
72 72
73 73 # Print debug info on what is happening to the console.
74 74 debug = False
75 75
76 76 # The title of the terminal, as captured through the ANSI escape
77 77 # sequences.
78 78 def _set_title(self, title):
79 79 return self.Parent.SetTitle(title)
80 80
81 81 def _get_title(self):
82 82 return self.Parent.GetTitle()
83 83
84 84 title = property(_get_title, _set_title)
85 85
86 86
87 87 # The buffer being edited.
88 88 # We are duplicating the definition here because of multiple
89 89 # inheritence
90 90 def _set_input_buffer(self, string):
91 91 ConsoleWidget._set_input_buffer(self, string)
92 92 self._colorize_input_buffer()
93 93
94 94 def _get_input_buffer(self):
95 95 """ Returns the text in current edit buffer.
96 96 """
97 97 return ConsoleWidget._get_input_buffer(self)
98 98
99 99 input_buffer = property(_get_input_buffer, _set_input_buffer)
100 100
101 101
102 102 #--------------------------------------------------------------------------
103 103 # Private Attributes
104 104 #--------------------------------------------------------------------------
105 105
106 106 # A flag governing the behavior of the input. Can be:
107 107 #
108 108 # 'readline' for readline-like behavior with a prompt
109 109 # and an edit buffer.
110 110 # 'raw_input' similar to readline, but triggered by a raw-input
111 111 # call. Can be used by subclasses to act differently.
112 112 # 'subprocess' for sending the raw input directly to a
113 113 # subprocess.
114 114 # 'buffering' for buffering of the input, that will be used
115 115 # when the input state switches back to another state.
116 116 _input_state = 'readline'
117 117
118 118 # Attribute to store reference to the pipes of a subprocess, if we
119 119 # are running any.
120 120 _running_process = False
121 121
122 122 # A queue for writing fast streams to the screen without flooding the
123 123 # event loop
124 124 _out_buffer = []
125 125
126 126 # A lock to lock the _out_buffer to make sure we don't empty it
127 127 # while it is being swapped
128 128 _out_buffer_lock = Lock()
129 129
130 130 # The different line markers used to higlight the prompts.
131 131 _markers = dict()
132 132
133 133 #--------------------------------------------------------------------------
134 134 # Public API
135 135 #--------------------------------------------------------------------------
136 136
137 137 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
138 138 size=wx.DefaultSize,
139 139 style=wx.CLIP_CHILDREN|wx.WANTS_CHARS,
140 140 *args, **kwds):
141 141 """ Create Shell instance.
142 142 """
143 143 ConsoleWidget.__init__(self, parent, id, pos, size, style)
144 144 PrefilterFrontEnd.__init__(self, **kwds)
145 145
146 146 # Stick in our own raw_input:
147 147 self.ipython0.raw_input = self.raw_input
148 148
149 149 # Marker for complete buffer.
150 150 self.MarkerDefine(_COMPLETE_BUFFER_MARKER, stc.STC_MARK_BACKGROUND,
151 151 background=_COMPLETE_BUFFER_BG)
152 152 # Marker for current input buffer.
153 153 self.MarkerDefine(_INPUT_MARKER, stc.STC_MARK_BACKGROUND,
154 154 background=_INPUT_BUFFER_BG)
155 155 # Marker for tracebacks.
156 156 self.MarkerDefine(_ERROR_MARKER, stc.STC_MARK_BACKGROUND,
157 157 background=_ERROR_BG)
158 158
159 159 # A time for flushing the write buffer
160 160 BUFFER_FLUSH_TIMER_ID = 100
161 161 self._buffer_flush_timer = wx.Timer(self, BUFFER_FLUSH_TIMER_ID)
162 162 wx.EVT_TIMER(self, BUFFER_FLUSH_TIMER_ID, self._buffer_flush)
163 163
164 164 if 'debug' in kwds:
165 165 self.debug = kwds['debug']
166 166 kwds.pop('debug')
167 167
168 168 # Inject self in namespace, for debug
169 169 if self.debug:
170 170 self.shell.user_ns['self'] = self
171 171 # Inject our own raw_input in namespace
172 172 self.shell.user_ns['raw_input'] = self.raw_input
173 173
174 174
175 175 def raw_input(self, prompt=''):
176 176 """ A replacement from python's raw_input.
177 177 """
178 178 self.new_prompt(prompt)
179 179 self._input_state = 'raw_input'
180 180 if hasattr(self, '_cursor'):
181 181 del self._cursor
182 182 self.SetCursor(wx.StockCursor(wx.CURSOR_CROSS))
183 183 self.__old_on_enter = self._on_enter
184 184 event_loop = wx.EventLoop()
185 185 def my_on_enter():
186 186 event_loop.Exit()
187 187 self._on_enter = my_on_enter
188 188 # XXX: Running a separate event_loop. Ugly.
189 189 event_loop.Run()
190 190 self._on_enter = self.__old_on_enter
191 191 self._input_state = 'buffering'
192 192 self._cursor = wx.BusyCursor()
193 193 return self.input_buffer.rstrip('\n')
194 194
195 195
196 196 def system_call(self, command_string):
197 197 self._input_state = 'subprocess'
198 198 event_loop = wx.EventLoop()
199 199 def _end_system_call():
200 200 self._input_state = 'buffering'
201 201 self._running_process = False
202 202 event_loop.Exit()
203 203
204 204 self._running_process = PipedProcess(command_string,
205 205 out_callback=self.buffered_write,
206 206 end_callback = _end_system_call)
207 207 self._running_process.start()
208 208 # XXX: Running a separate event_loop. Ugly.
209 209 event_loop.Run()
210 210 # Be sure to flush the buffer.
211 211 self._buffer_flush(event=None)
212 212
213 213
214 214 def do_calltip(self):
215 215 """ Analyse current and displays useful calltip for it.
216 216 """
217 217 if self.debug:
218 218 print >>sys.__stdout__, "do_calltip"
219 219 separators = re.compile('[\s\{\}\[\]\(\)\= ,:]')
220 220 symbol = self.input_buffer
221 221 symbol_string = separators.split(symbol)[-1]
222 222 base_symbol_string = symbol_string.split('.')[0]
223 223 if base_symbol_string in self.shell.user_ns:
224 224 symbol = self.shell.user_ns[base_symbol_string]
225 225 elif base_symbol_string in self.shell.user_global_ns:
226 226 symbol = self.shell.user_global_ns[base_symbol_string]
227 227 elif base_symbol_string in __builtin__.__dict__:
228 228 symbol = __builtin__.__dict__[base_symbol_string]
229 229 else:
230 230 return False
231 231 try:
232 232 for name in symbol_string.split('.')[1:] + ['__doc__']:
233 233 symbol = getattr(symbol, name)
234 234 self.AutoCompCancel()
235 235 # Check that the symbol can indeed be converted to a string:
236 236 symbol += ''
237 237 wx.CallAfter(self.CallTipShow, self.GetCurrentPos(), symbol)
238 238 except:
239 239 # The retrieve symbol couldn't be converted to a string
240 240 pass
241 241
242 242
243 243 def _popup_completion(self, create=False):
244 244 """ Updates the popup completion menu if it exists. If create is
245 245 true, open the menu.
246 246 """
247 247 if self.debug:
248 248 print >>sys.__stdout__, "_popup_completion"
249 249 line = self.input_buffer
250 250 if (self.AutoCompActive() and line and not line[-1] == '.') \
251 251 or create==True:
252 252 suggestion, completions = self.complete(line)
253 253 offset=0
254 254 if completions:
255 255 complete_sep = re.compile('[\s\{\}\[\]\(\)\= ,:]')
256 256 residual = complete_sep.split(line)[-1]
257 257 offset = len(residual)
258 258 self.pop_completion(completions, offset=offset)
259 259 if self.debug:
260 260 print >>sys.__stdout__, completions
261 261
262 262
263 263 def buffered_write(self, text):
264 264 """ A write method for streams, that caches the stream in order
265 265 to avoid flooding the event loop.
266 266
267 267 This can be called outside of the main loop, in separate
268 268 threads.
269 269 """
270 270 self._out_buffer_lock.acquire()
271 271 self._out_buffer.append(text)
272 272 self._out_buffer_lock.release()
273 273 if not self._buffer_flush_timer.IsRunning():
274 274 wx.CallAfter(self._buffer_flush_timer.Start,
275 275 milliseconds=100, oneShot=True)
276 276
277 277
278 278 #--------------------------------------------------------------------------
279 279 # LineFrontEnd interface
280 280 #--------------------------------------------------------------------------
281 281
282 282 def execute(self, python_string, raw_string=None):
283 283 self._input_state = 'buffering'
284 284 self.CallTipCancel()
285 285 self._cursor = wx.BusyCursor()
286 286 if raw_string is None:
287 287 raw_string = python_string
288 288 end_line = self.current_prompt_line \
289 289 + max(1, len(raw_string.split('\n'))-1)
290 290 for i in range(self.current_prompt_line, end_line):
291 291 if i in self._markers:
292 292 self.MarkerDeleteHandle(self._markers[i])
293 293 self._markers[i] = self.MarkerAdd(i, _COMPLETE_BUFFER_MARKER)
294 294 # Use a callafter to update the display robustly under windows
295 295 def callback():
296 296 self.GotoPos(self.GetLength())
297 297 PrefilterFrontEnd.execute(self, python_string,
298 298 raw_string=raw_string)
299 299 wx.CallAfter(callback)
300 300
301 301
302 302 def execute_command(self, command, hidden=False):
303 303 """ Execute a command, not only in the model, but also in the
304 304 view.
305 305 """
306 306 if hidden:
307 307 return self.shell.execute(command)
308 308 else:
309 309 # XXX: we are not storing the input buffer previous to the
310 310 # execution, as this forces us to run the execution
311 311 # input_buffer a yield, which is not good.
312 312 ##current_buffer = self.shell.control.input_buffer
313 313 command = command.rstrip()
314 314 if len(command.split('\n')) > 1:
315 315 # The input command is several lines long, we need to
316 316 # force the execution to happen
317 317 command += '\n'
318 318 cleaned_command = self.prefilter_input(command)
319 319 self.input_buffer = command
320 320 # Do not use wx.Yield() (aka GUI.process_events()) to avoid
321 321 # recursive yields.
322 322 self.ProcessEvent(wx.PaintEvent())
323 323 self.write('\n')
324 324 if not self.is_complete(cleaned_command + '\n'):
325 325 self._colorize_input_buffer()
326 326 self.render_error('Incomplete or invalid input')
327 327 self.new_prompt(self.input_prompt_template.substitute(
328 328 number=(self.last_result['number'] + 1)))
329 329 return False
330 330 self._on_enter()
331 331 return True
332 332
333 333
334 334 def save_output_hooks(self):
335 335 self.__old_raw_input = __builtin__.raw_input
336 336 PrefilterFrontEnd.save_output_hooks(self)
337 337
338 338 def capture_output(self):
339 339 self.SetLexer(stc.STC_LEX_NULL)
340 340 PrefilterFrontEnd.capture_output(self)
341 341 __builtin__.raw_input = self.raw_input
342 342
343 343
344 344 def release_output(self):
345 345 __builtin__.raw_input = self.__old_raw_input
346 346 PrefilterFrontEnd.release_output(self)
347 347 self.SetLexer(stc.STC_LEX_PYTHON)
348 348
349 349
350 350 def after_execute(self):
351 351 PrefilterFrontEnd.after_execute(self)
352 352 # Clear the wait cursor
353 353 if hasattr(self, '_cursor'):
354 354 del self._cursor
355 355 self.SetCursor(wx.StockCursor(wx.CURSOR_CHAR))
356 356
357 357
358 358 def show_traceback(self):
359 359 start_line = self.GetCurrentLine()
360 360 PrefilterFrontEnd.show_traceback(self)
361 361 self.ProcessEvent(wx.PaintEvent())
362 362 #wx.Yield()
363 363 for i in range(start_line, self.GetCurrentLine()):
364 364 self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER)
365 365
366 366
367 367 #--------------------------------------------------------------------------
368 368 # FrontEndBase interface
369 369 #--------------------------------------------------------------------------
370 370
371 371 def render_error(self, e):
372 372 start_line = self.GetCurrentLine()
373 373 self.write('\n' + e + '\n')
374 374 for i in range(start_line, self.GetCurrentLine()):
375 375 self._markers[i] = self.MarkerAdd(i, _ERROR_MARKER)
376 376
377 377
378 378 #--------------------------------------------------------------------------
379 379 # ConsoleWidget interface
380 380 #--------------------------------------------------------------------------
381 381
382 382 def new_prompt(self, prompt):
383 383 """ Display a new prompt, and start a new input buffer.
384 384 """
385 385 self._input_state = 'readline'
386 386 ConsoleWidget.new_prompt(self, prompt)
387 387 i = self.current_prompt_line
388 388 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
389 389
390 390
391 391 def write(self, *args, **kwargs):
392 392 # Avoid multiple inheritence, be explicit about which
393 393 # parent method class gets called
394 394 ConsoleWidget.write(self, *args, **kwargs)
395 395
396 396
397 397 def _on_key_down(self, event, skip=True):
398 398 """ Capture the character events, let the parent
399 399 widget handle them, and put our logic afterward.
400 400 """
401 401 # FIXME: This method needs to be broken down in smaller ones.
402 402 current_line_number = self.GetCurrentLine()
403 403 if event.KeyCode in (ord('c'), ord('C')) and event.ControlDown():
404 404 # Capture Control-C
405 405 if self._input_state == 'subprocess':
406 406 if self.debug:
407 407 print >>sys.__stderr__, 'Killing running process'
408 408 if hasattr(self._running_process, 'process'):
409 409 self._running_process.process.kill()
410 410 elif self._input_state == 'buffering':
411 411 if self.debug:
412 412 print >>sys.__stderr__, 'Raising KeyboardInterrupt'
413 413 raise KeyboardInterrupt
414 414 # XXX: We need to make really sure we
415 415 # get back to a prompt.
416 416 elif self._input_state == 'subprocess' and (
417 417 ( event.KeyCode<256 and
418 418 not event.ControlDown() )
419 419 or
420 420 ( event.KeyCode in (ord('d'), ord('D')) and
421 421 event.ControlDown())):
422 422 # We are running a process, we redirect keys.
423 423 ConsoleWidget._on_key_down(self, event, skip=skip)
424 424 char = chr(event.KeyCode)
425 425 # Deal with some inconsistency in wx keycodes:
426 426 if char == '\r':
427 427 char = '\n'
428 428 elif not event.ShiftDown():
429 429 char = char.lower()
430 430 if event.ControlDown() and event.KeyCode in (ord('d'), ord('D')):
431 431 char = '\04'
432 432 self._running_process.process.stdin.write(char)
433 433 self._running_process.process.stdin.flush()
434 434 elif event.KeyCode in (ord('('), 57, 53):
435 435 # Calltips
436 436 event.Skip()
437 437 self.do_calltip()
438 438 elif self.AutoCompActive() and not event.KeyCode == ord('\t'):
439 439 event.Skip()
440 440 if event.KeyCode in (wx.WXK_BACK, wx.WXK_DELETE):
441 441 wx.CallAfter(self._popup_completion, create=True)
442 442 elif not event.KeyCode in (wx.WXK_UP, wx.WXK_DOWN, wx.WXK_LEFT,
443 443 wx.WXK_RIGHT, wx.WXK_ESCAPE):
444 444 wx.CallAfter(self._popup_completion)
445 445 else:
446 446 # Up history
447 447 if event.KeyCode == wx.WXK_UP and (
448 448 ( current_line_number == self.current_prompt_line and
449 449 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
450 450 or event.ControlDown() ):
451 451 new_buffer = self.get_history_previous(
452 452 self.input_buffer)
453 453 if new_buffer is not None:
454 454 self.input_buffer = new_buffer
455 455 if self.GetCurrentLine() > self.current_prompt_line:
456 456 # Go to first line, for seemless history up.
457 457 self.GotoPos(self.current_prompt_pos)
458 458 # Down history
459 459 elif event.KeyCode == wx.WXK_DOWN and (
460 460 ( current_line_number == self.LineCount -1 and
461 461 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN) )
462 462 or event.ControlDown() ):
463 463 new_buffer = self.get_history_next()
464 464 if new_buffer is not None:
465 465 self.input_buffer = new_buffer
466 466 # Tab-completion
467 467 elif event.KeyCode == ord('\t'):
468 468 current_line, current_line_number = self.CurLine
469 469 if not re.match(r'^\s*$', current_line):
470 470 self.complete_current_input()
471 471 if self.AutoCompActive():
472 472 wx.CallAfter(self._popup_completion, create=True)
473 473 else:
474 474 event.Skip()
475 475 else:
476 476 ConsoleWidget._on_key_down(self, event, skip=skip)
477 477
478 478
479 479 def _on_key_up(self, event, skip=True):
480 480 """ Called when any key is released.
481 481 """
482 482 if event.KeyCode in (59, ord('.')):
483 483 # Intercepting '.'
484 484 event.Skip()
485 485 wx.CallAfter(self._popup_completion, create=True)
486 486 else:
487 487 ConsoleWidget._on_key_up(self, event, skip=skip)
488 if (self.input_buffer.split('\n')[-1] == self.continuation_prompt()
489 and self._input_state == 'readline'):
490 # Make sure the continuation_prompt is followed by a whitespace
491 position = self.GetCurrentPos()
492 self.input_buffer += ' '
493 self.GotoPos(position)
488 494
489 495
490 496 def _on_enter(self):
491 497 """ Called on return key down, in readline input_state.
492 498 """
493 499 if self.debug:
494 500 print >>sys.__stdout__, repr(self.input_buffer)
495 501 PrefilterFrontEnd._on_enter(self)
496 502
497 503
498 504 #--------------------------------------------------------------------------
499 505 # EditWindow API
500 506 #--------------------------------------------------------------------------
501 507
502 508 def OnUpdateUI(self, event):
503 509 """ Override the OnUpdateUI of the EditWindow class, to prevent
504 510 syntax highlighting both for faster redraw, and for more
505 511 consistent look and feel.
506 512 """
507 513 if not self._input_state == 'readline':
508 514 ConsoleWidget.OnUpdateUI(self, event)
509 515
510 516 #--------------------------------------------------------------------------
511 517 # Private API
512 518 #--------------------------------------------------------------------------
513 519
514 520 def _buffer_flush(self, event):
515 521 """ Called by the timer to flush the write buffer.
516 522
517 523 This is always called in the mainloop, by the wx timer.
518 524 """
519 525 self._out_buffer_lock.acquire()
520 526 _out_buffer = self._out_buffer
521 527 self._out_buffer = []
522 528 self._out_buffer_lock.release()
523 529 self.write(''.join(_out_buffer), refresh=False)
524 530
525 531
526 532 def _colorize_input_buffer(self):
527 533 """ Keep the input buffer lines at a bright color.
528 534 """
529 535 if not self._input_state in ('readline', 'raw_input'):
530 536 return
531 537 end_line = self.GetCurrentLine()
532 538 if not sys.platform == 'win32':
533 539 end_line += 1
534 540 for i in range(self.current_prompt_line, end_line):
535 541 if i in self._markers:
536 542 self.MarkerDeleteHandle(self._markers[i])
537 543 self._markers[i] = self.MarkerAdd(i, _INPUT_MARKER)
538 544
539 545
540 546 if __name__ == '__main__':
541 547 class MainWindow(wx.Frame):
542 548 def __init__(self, parent, id, title):
543 549 wx.Frame.__init__(self, parent, id, title, size=(300,250))
544 550 self._sizer = wx.BoxSizer(wx.VERTICAL)
545 551 self.shell = WxController(self)
546 552 self._sizer.Add(self.shell, 1, wx.EXPAND)
547 553 self.SetSizer(self._sizer)
548 554 self.SetAutoLayout(1)
549 555 self.Show(True)
550 556
551 557 app = wx.PySimpleApp()
552 558 frame = MainWindow(None, wx.ID_ANY, 'Ipython')
553 559 frame.shell.SetFocus()
554 560 frame.SetSize((680, 460))
555 561 self = frame.shell
556 562
557 563 app.MainLoop()
558 564
General Comments 0
You need to be logged in to leave comments. Login now