##// END OF EJS Templates
Fixed bug where replacement prompts were occasionally inserted at inappropriate times.
epatters -
Show More
@@ -1,323 +1,323 b''
1 # Standard library imports
1 # Standard library imports
2 from subprocess import Popen
2 from subprocess import Popen
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7 # Local imports
7 # Local imports
8 from IPython.core.inputsplitter import IPythonInputSplitter
8 from IPython.core.inputsplitter import IPythonInputSplitter
9 from IPython.core.usage import default_banner
9 from IPython.core.usage import default_banner
10 from frontend_widget import FrontendWidget
10 from frontend_widget import FrontendWidget
11
11
12
12
13 class IPythonPromptBlock(object):
13 class IPythonPromptBlock(object):
14 """ An internal storage object for IPythonWidget.
14 """ An internal storage object for IPythonWidget.
15 """
15 """
16 def __init__(self, block, length, number):
16 def __init__(self, block, length, number):
17 self.block = block
17 self.block = block
18 self.length = length
18 self.length = length
19 self.number = number
19 self.number = number
20
20
21
21
22 class IPythonWidget(FrontendWidget):
22 class IPythonWidget(FrontendWidget):
23 """ A FrontendWidget for an IPython kernel.
23 """ A FrontendWidget for an IPython kernel.
24 """
24 """
25
25
26 # Signal emitted when an editor is needed for a file and the editor has been
26 # Signal emitted when an editor is needed for a file and the editor has been
27 # specified as 'custom'. See 'set_editor' for more information.
27 # specified as 'custom'. See 'set_editor' for more information.
28 custom_edit_requested = QtCore.pyqtSignal(object, object)
28 custom_edit_requested = QtCore.pyqtSignal(object, object)
29
29
30 # The default stylesheet: black text on a white background.
30 # The default stylesheet: black text on a white background.
31 default_stylesheet = """
31 default_stylesheet = """
32 .error { color: red; }
32 .error { color: red; }
33 .in-prompt { color: navy; }
33 .in-prompt { color: navy; }
34 .in-prompt-number { font-weight: bold; }
34 .in-prompt-number { font-weight: bold; }
35 .out-prompt { color: darkred; }
35 .out-prompt { color: darkred; }
36 .out-prompt-number { font-weight: bold; }
36 .out-prompt-number { font-weight: bold; }
37 """
37 """
38
38
39 # A dark stylesheet: white text on a black background.
39 # A dark stylesheet: white text on a black background.
40 dark_stylesheet = """
40 dark_stylesheet = """
41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
41 QPlainTextEdit, QTextEdit { background-color: black; color: white }
42 QFrame { border: 1px solid grey; }
42 QFrame { border: 1px solid grey; }
43 .error { color: red; }
43 .error { color: red; }
44 .in-prompt { color: lime; }
44 .in-prompt { color: lime; }
45 .in-prompt-number { color: lime; font-weight: bold; }
45 .in-prompt-number { color: lime; font-weight: bold; }
46 .out-prompt { color: red; }
46 .out-prompt { color: red; }
47 .out-prompt-number { color: red; font-weight: bold; }
47 .out-prompt-number { color: red; font-weight: bold; }
48 """
48 """
49
49
50 # Default prompts.
50 # Default prompts.
51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
51 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
52 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
53
53
54 # FrontendWidget protected class variables.
54 # FrontendWidget protected class variables.
55 #_input_splitter_class = IPythonInputSplitter
55 #_input_splitter_class = IPythonInputSplitter
56
56
57 # IPythonWidget protected class variables.
57 # IPythonWidget protected class variables.
58 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
58 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
59 _payload_source_page = 'IPython.zmq.page.page'
59 _payload_source_page = 'IPython.zmq.page.page'
60
60
61 #---------------------------------------------------------------------------
61 #---------------------------------------------------------------------------
62 # 'object' interface
62 # 'object' interface
63 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
64
64
65 def __init__(self, *args, **kw):
65 def __init__(self, *args, **kw):
66 super(IPythonWidget, self).__init__(*args, **kw)
66 super(IPythonWidget, self).__init__(*args, **kw)
67
67
68 # IPythonWidget protected variables.
68 # IPythonWidget protected variables.
69 self._previous_prompt_obj = None
69 self._previous_prompt_obj = None
70
70
71 # Set a default editor and stylesheet.
71 # Set a default editor and stylesheet.
72 self.set_editor('default')
72 self.set_editor('default')
73 self.reset_styling()
73 self.reset_styling()
74
74
75 #---------------------------------------------------------------------------
75 #---------------------------------------------------------------------------
76 # 'BaseFrontendMixin' abstract interface
76 # 'BaseFrontendMixin' abstract interface
77 #---------------------------------------------------------------------------
77 #---------------------------------------------------------------------------
78
78
79 def _handle_history_reply(self, msg):
79 def _handle_history_reply(self, msg):
80 """ Implemented to handle history replies, which are only supported by
80 """ Implemented to handle history replies, which are only supported by
81 the IPython kernel.
81 the IPython kernel.
82 """
82 """
83 history_dict = msg['content']['history']
83 history_dict = msg['content']['history']
84 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
84 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
85 self._set_history(items)
85 self._set_history(items)
86
86
87 def _handle_prompt_reply(self, msg):
87 def _handle_prompt_reply(self, msg):
88 """ Implemented to handle prompt number replies, which are only
88 """ Implemented to handle prompt number replies, which are only
89 supported by the IPython kernel.
89 supported by the IPython kernel.
90 """
90 """
91 content = msg['content']
91 content = msg['content']
92 self._show_interpreter_prompt(content['prompt_number'],
92 self._show_interpreter_prompt(content['prompt_number'],
93 content['input_sep'])
93 content['input_sep'])
94
94
95 def _handle_pyout(self, msg):
95 def _handle_pyout(self, msg):
96 """ Reimplemented for IPython-style "display hook".
96 """ Reimplemented for IPython-style "display hook".
97 """
97 """
98 if not self._hidden and self._is_from_this_session(msg):
98 if not self._hidden and self._is_from_this_session(msg):
99 content = msg['content']
99 content = msg['content']
100 prompt_number = content['prompt_number']
100 prompt_number = content['prompt_number']
101 self._append_plain_text(content['output_sep'])
101 self._append_plain_text(content['output_sep'])
102 self._append_html(self._make_out_prompt(prompt_number))
102 self._append_html(self._make_out_prompt(prompt_number))
103 self._append_plain_text(content['data'] + '\n' +
103 self._append_plain_text(content['data'] + '\n' +
104 content['output_sep2'])
104 content['output_sep2'])
105
105
106 def _started_channels(self):
106 def _started_channels(self):
107 """ Reimplemented to make a history request.
107 """ Reimplemented to make a history request.
108 """
108 """
109 super(IPythonWidget, self)._started_channels()
109 super(IPythonWidget, self)._started_channels()
110 # FIXME: Disabled until history requests are properly implemented.
110 # FIXME: Disabled until history requests are properly implemented.
111 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
111 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
112
112
113 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
114 # 'FrontendWidget' interface
114 # 'FrontendWidget' interface
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116
116
117 def execute_file(self, path, hidden=False):
117 def execute_file(self, path, hidden=False):
118 """ Reimplemented to use the 'run' magic.
118 """ Reimplemented to use the 'run' magic.
119 """
119 """
120 self.execute('%%run %s' % path, hidden=hidden)
120 self.execute('%%run %s' % path, hidden=hidden)
121
121
122 #---------------------------------------------------------------------------
122 #---------------------------------------------------------------------------
123 # 'FrontendWidget' protected interface
123 # 'FrontendWidget' protected interface
124 #---------------------------------------------------------------------------
124 #---------------------------------------------------------------------------
125
125
126 def _get_banner(self):
126 def _get_banner(self):
127 """ Reimplemented to return IPython's default banner.
127 """ Reimplemented to return IPython's default banner.
128 """
128 """
129 return default_banner + '\n'
129 return default_banner + '\n'
130
130
131 def _process_execute_error(self, msg):
131 def _process_execute_error(self, msg):
132 """ Reimplemented for IPython-style traceback formatting.
132 """ Reimplemented for IPython-style traceback formatting.
133 """
133 """
134 content = msg['content']
134 content = msg['content']
135 traceback = '\n'.join(content['traceback']) + '\n'
135 traceback = '\n'.join(content['traceback']) + '\n'
136 if False:
136 if False:
137 # FIXME: For now, tracebacks come as plain text, so we can't use
137 # FIXME: For now, tracebacks come as plain text, so we can't use
138 # the html renderer yet. Once we refactor ultratb to produce
138 # the html renderer yet. Once we refactor ultratb to produce
139 # properly styled tracebacks, this branch should be the default
139 # properly styled tracebacks, this branch should be the default
140 traceback = traceback.replace(' ', '&nbsp;')
140 traceback = traceback.replace(' ', '&nbsp;')
141 traceback = traceback.replace('\n', '<br/>')
141 traceback = traceback.replace('\n', '<br/>')
142
142
143 ename = content['ename']
143 ename = content['ename']
144 ename_styled = '<span class="error">%s</span>' % ename
144 ename_styled = '<span class="error">%s</span>' % ename
145 traceback = traceback.replace(ename, ename_styled)
145 traceback = traceback.replace(ename, ename_styled)
146
146
147 self._append_html(traceback)
147 self._append_html(traceback)
148 else:
148 else:
149 # This is the fallback for now, using plain text with ansi escapes
149 # This is the fallback for now, using plain text with ansi escapes
150 self._append_plain_text(traceback)
150 self._append_plain_text(traceback)
151
151
152 def _process_execute_payload(self, item):
152 def _process_execute_payload(self, item):
153 """ Reimplemented to handle %edit and paging payloads.
153 """ Reimplemented to handle %edit and paging payloads.
154 """
154 """
155 if item['source'] == self._payload_source_edit:
155 if item['source'] == self._payload_source_edit:
156 self.edit(item['filename'], item['line_number'])
156 self.edit(item['filename'], item['line_number'])
157 return True
157 return True
158 elif item['source'] == self._payload_source_page:
158 elif item['source'] == self._payload_source_page:
159 self._page(item['data'])
159 self._page(item['data'])
160 return True
160 return True
161 else:
161 else:
162 return False
162 return False
163
163
164 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
164 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
165 """ Reimplemented for IPython-style prompts.
165 """ Reimplemented for IPython-style prompts.
166 """
166 """
167 # If a number was not specified, make a prompt number request.
167 # If a number was not specified, make a prompt number request.
168 if number is None:
168 if number is None:
169 self.kernel_manager.xreq_channel.prompt()
169 self.kernel_manager.xreq_channel.prompt()
170 return
170 return
171
171
172 # Show a new prompt and save information about it so that it can be
172 # Show a new prompt and save information about it so that it can be
173 # updated later if the prompt number turns out to be wrong.
173 # updated later if the prompt number turns out to be wrong.
174 self._append_plain_text(input_sep)
174 self._append_plain_text(input_sep)
175 self._show_prompt(self._make_in_prompt(number), html=True)
175 self._show_prompt(self._make_in_prompt(number), html=True)
176 block = self._control.document().lastBlock()
176 block = self._control.document().lastBlock()
177 length = len(self._prompt)
177 length = len(self._prompt)
178 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
178 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
179
179
180 # Update continuation prompt to reflect (possibly) new prompt length.
180 # Update continuation prompt to reflect (possibly) new prompt length.
181 self._set_continuation_prompt(
181 self._set_continuation_prompt(
182 self._make_continuation_prompt(self._prompt), html=True)
182 self._make_continuation_prompt(self._prompt), html=True)
183
183
184 def _show_interpreter_prompt_for_reply(self, msg):
184 def _show_interpreter_prompt_for_reply(self, msg):
185 """ Reimplemented for IPython-style prompts.
185 """ Reimplemented for IPython-style prompts.
186 """
186 """
187 # Update the old prompt number if necessary.
187 # Update the old prompt number if necessary.
188 content = msg['content']
188 content = msg['content']
189 previous_prompt_number = content['prompt_number']
189 previous_prompt_number = content['prompt_number']
190 if self._previous_prompt_obj and \
190 if self._previous_prompt_obj and \
191 self._previous_prompt_obj.number != previous_prompt_number:
191 self._previous_prompt_obj.number != previous_prompt_number:
192 block = self._previous_prompt_obj.block
192 block = self._previous_prompt_obj.block
193 if block.isValid():
193 if block.isValid() and not block.text().isEmpty():
194
194
195 # Remove the old prompt and insert a new prompt.
195 # Remove the old prompt and insert a new prompt.
196 cursor = QtGui.QTextCursor(block)
196 cursor = QtGui.QTextCursor(block)
197 cursor.movePosition(QtGui.QTextCursor.Right,
197 cursor.movePosition(QtGui.QTextCursor.Right,
198 QtGui.QTextCursor.KeepAnchor,
198 QtGui.QTextCursor.KeepAnchor,
199 self._previous_prompt_obj.length)
199 self._previous_prompt_obj.length)
200 prompt = self._make_in_prompt(previous_prompt_number)
200 prompt = self._make_in_prompt(previous_prompt_number)
201 self._prompt = self._insert_html_fetching_plain_text(
201 self._prompt = self._insert_html_fetching_plain_text(
202 cursor, prompt)
202 cursor, prompt)
203
203
204 # When the HTML is inserted, Qt blows away the syntax
204 # When the HTML is inserted, Qt blows away the syntax
205 # highlighting for the line, so we need to rehighlight it.
205 # highlighting for the line, so we need to rehighlight it.
206 self._highlighter.rehighlightBlock(cursor.block())
206 self._highlighter.rehighlightBlock(cursor.block())
207
207
208 self._previous_prompt_obj = None
208 self._previous_prompt_obj = None
209
209
210 # Show a new prompt with the kernel's estimated prompt number.
210 # Show a new prompt with the kernel's estimated prompt number.
211 next_prompt = content['next_prompt']
211 next_prompt = content['next_prompt']
212 self._show_interpreter_prompt(next_prompt['prompt_number'],
212 self._show_interpreter_prompt(next_prompt['prompt_number'],
213 next_prompt['input_sep'])
213 next_prompt['input_sep'])
214
214
215 #---------------------------------------------------------------------------
215 #---------------------------------------------------------------------------
216 # 'IPythonWidget' interface
216 # 'IPythonWidget' interface
217 #---------------------------------------------------------------------------
217 #---------------------------------------------------------------------------
218
218
219 def edit(self, filename, line=None):
219 def edit(self, filename, line=None):
220 """ Opens a Python script for editing.
220 """ Opens a Python script for editing.
221
221
222 Parameters:
222 Parameters:
223 -----------
223 -----------
224 filename : str
224 filename : str
225 A path to a local system file.
225 A path to a local system file.
226
226
227 line : int, optional
227 line : int, optional
228 A line of interest in the file.
228 A line of interest in the file.
229
229
230 Raises:
230 Raises:
231 -------
231 -------
232 OSError
232 OSError
233 If the editor command cannot be executed.
233 If the editor command cannot be executed.
234 """
234 """
235 if self._editor == 'default':
235 if self._editor == 'default':
236 url = QtCore.QUrl.fromLocalFile(filename)
236 url = QtCore.QUrl.fromLocalFile(filename)
237 if not QtGui.QDesktopServices.openUrl(url):
237 if not QtGui.QDesktopServices.openUrl(url):
238 message = 'Failed to open %s with the default application'
238 message = 'Failed to open %s with the default application'
239 raise OSError(message % repr(filename))
239 raise OSError(message % repr(filename))
240 elif self._editor is None:
240 elif self._editor is None:
241 self.custom_edit_requested.emit(filename, line)
241 self.custom_edit_requested.emit(filename, line)
242 else:
242 else:
243 Popen(self._editor + [filename])
243 Popen(self._editor + [filename])
244
244
245 def reset_styling(self):
245 def reset_styling(self):
246 """ Restores the default IPythonWidget styling.
246 """ Restores the default IPythonWidget styling.
247 """
247 """
248 self.set_styling(self.default_stylesheet, syntax_style='default')
248 self.set_styling(self.default_stylesheet, syntax_style='default')
249 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
249 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
250
250
251 def set_editor(self, editor):
251 def set_editor(self, editor):
252 """ Sets the editor to use with the %edit magic.
252 """ Sets the editor to use with the %edit magic.
253
253
254 Parameters:
254 Parameters:
255 -----------
255 -----------
256 editor : str or sequence of str
256 editor : str or sequence of str
257 A command suitable for use with Popen. This command will be executed
257 A command suitable for use with Popen. This command will be executed
258 with a single argument--a filename--when editing is requested.
258 with a single argument--a filename--when editing is requested.
259
259
260 This parameter also takes two special values:
260 This parameter also takes two special values:
261 'default' : Files will be edited with the system default
261 'default' : Files will be edited with the system default
262 application for Python files.
262 application for Python files.
263 'custom' : Emit a 'custom_edit_requested(str, int)' signal
263 'custom' : Emit a 'custom_edit_requested(str, int)' signal
264 instead of opening an editor.
264 instead of opening an editor.
265 """
265 """
266 if editor == 'default':
266 if editor == 'default':
267 self._editor = 'default'
267 self._editor = 'default'
268 elif editor == 'custom':
268 elif editor == 'custom':
269 self._editor = None
269 self._editor = None
270 elif isinstance(editor, basestring):
270 elif isinstance(editor, basestring):
271 self._editor = [ editor ]
271 self._editor = [ editor ]
272 else:
272 else:
273 self._editor = list(editor)
273 self._editor = list(editor)
274
274
275 def set_styling(self, stylesheet, syntax_style=None):
275 def set_styling(self, stylesheet, syntax_style=None):
276 """ Sets the IPythonWidget styling.
276 """ Sets the IPythonWidget styling.
277
277
278 Parameters:
278 Parameters:
279 -----------
279 -----------
280 stylesheet : str
280 stylesheet : str
281 A CSS stylesheet. The stylesheet can contain classes for:
281 A CSS stylesheet. The stylesheet can contain classes for:
282 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
282 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
283 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
283 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
284 3. IPython: .error, .in-prompt, .out-prompt, etc.
284 3. IPython: .error, .in-prompt, .out-prompt, etc.
285
285
286 syntax_style : str or None [default None]
286 syntax_style : str or None [default None]
287 If specified, use the Pygments style with given name. Otherwise,
287 If specified, use the Pygments style with given name. Otherwise,
288 the stylesheet is queried for Pygments style information.
288 the stylesheet is queried for Pygments style information.
289 """
289 """
290 self.setStyleSheet(stylesheet)
290 self.setStyleSheet(stylesheet)
291 self._control.document().setDefaultStyleSheet(stylesheet)
291 self._control.document().setDefaultStyleSheet(stylesheet)
292 if self._page_control:
292 if self._page_control:
293 self._page_control.document().setDefaultStyleSheet(stylesheet)
293 self._page_control.document().setDefaultStyleSheet(stylesheet)
294
294
295 if syntax_style is None:
295 if syntax_style is None:
296 self._highlighter.set_style_sheet(stylesheet)
296 self._highlighter.set_style_sheet(stylesheet)
297 else:
297 else:
298 self._highlighter.set_style(syntax_style)
298 self._highlighter.set_style(syntax_style)
299
299
300 #---------------------------------------------------------------------------
300 #---------------------------------------------------------------------------
301 # 'IPythonWidget' protected interface
301 # 'IPythonWidget' protected interface
302 #---------------------------------------------------------------------------
302 #---------------------------------------------------------------------------
303
303
304 def _make_in_prompt(self, number):
304 def _make_in_prompt(self, number):
305 """ Given a prompt number, returns an HTML In prompt.
305 """ Given a prompt number, returns an HTML In prompt.
306 """
306 """
307 body = self.in_prompt % number
307 body = self.in_prompt % number
308 return '<span class="in-prompt">%s</span>' % body
308 return '<span class="in-prompt">%s</span>' % body
309
309
310 def _make_continuation_prompt(self, prompt):
310 def _make_continuation_prompt(self, prompt):
311 """ Given a plain text version of an In prompt, returns an HTML
311 """ Given a plain text version of an In prompt, returns an HTML
312 continuation prompt.
312 continuation prompt.
313 """
313 """
314 end_chars = '...: '
314 end_chars = '...: '
315 space_count = len(prompt.lstrip('\n')) - len(end_chars)
315 space_count = len(prompt.lstrip('\n')) - len(end_chars)
316 body = '&nbsp;' * space_count + end_chars
316 body = '&nbsp;' * space_count + end_chars
317 return '<span class="in-prompt">%s</span>' % body
317 return '<span class="in-prompt">%s</span>' % body
318
318
319 def _make_out_prompt(self, number):
319 def _make_out_prompt(self, number):
320 """ Given a prompt number, returns an HTML Out prompt.
320 """ Given a prompt number, returns an HTML Out prompt.
321 """
321 """
322 body = self.out_prompt % number
322 body = self.out_prompt % number
323 return '<span class="out-prompt">%s</span>' % body
323 return '<span class="out-prompt">%s</span>' % body
General Comments 0
You need to be logged in to leave comments. Login now