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