##// END OF EJS Templates
Fixed regressions in the pure Python kernel.
epatters -
Show More
@@ -1,1353 +1,1354 b''
1 # Standard library imports
1 # Standard library imports
2 import re
2 import re
3 import sys
3 import sys
4 from textwrap import dedent
4 from textwrap import dedent
5
5
6 # System library imports
6 # System library imports
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from ansi_code_processor import QtAnsiCodeProcessor
10 from ansi_code_processor import QtAnsiCodeProcessor
11 from completion_widget import CompletionWidget
11 from completion_widget import CompletionWidget
12
12
13
13
14 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
14 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
15 """ A QPlainTextEdit suitable for use with ConsoleWidget.
15 """ A QPlainTextEdit suitable for use with ConsoleWidget.
16 """
16 """
17 # Prevents text from being moved by drag and drop. Note that is not, for
17 # Prevents text from being moved by drag and drop. Note that is not, for
18 # some reason, sufficient to catch drag events in the ConsoleWidget's
18 # some reason, sufficient to catch drag events in the ConsoleWidget's
19 # event filter.
19 # event filter.
20 def dragEnterEvent(self, event): pass
20 def dragEnterEvent(self, event): pass
21 def dragLeaveEvent(self, event): pass
21 def dragLeaveEvent(self, event): pass
22 def dragMoveEvent(self, event): pass
22 def dragMoveEvent(self, event): pass
23 def dropEvent(self, event): pass
23 def dropEvent(self, event): pass
24
24
25 class ConsoleTextEdit(QtGui.QTextEdit):
25 class ConsoleTextEdit(QtGui.QTextEdit):
26 """ A QTextEdit suitable for use with ConsoleWidget.
26 """ A QTextEdit suitable for use with ConsoleWidget.
27 """
27 """
28 # See above.
28 # See above.
29 def dragEnterEvent(self, event): pass
29 def dragEnterEvent(self, event): pass
30 def dragLeaveEvent(self, event): pass
30 def dragLeaveEvent(self, event): pass
31 def dragMoveEvent(self, event): pass
31 def dragMoveEvent(self, event): pass
32 def dropEvent(self, event): pass
32 def dropEvent(self, event): pass
33
33
34
34
35 class ConsoleWidget(QtGui.QWidget):
35 class ConsoleWidget(QtGui.QWidget):
36 """ An abstract base class for console-type widgets. This class has
36 """ An abstract base class for console-type widgets. This class has
37 functionality for:
37 functionality for:
38
38
39 * Maintaining a prompt and editing region
39 * Maintaining a prompt and editing region
40 * Providing the traditional Unix-style console keyboard shortcuts
40 * Providing the traditional Unix-style console keyboard shortcuts
41 * Performing tab completion
41 * Performing tab completion
42 * Paging text
42 * Paging text
43 * Handling ANSI escape codes
43 * Handling ANSI escape codes
44
44
45 ConsoleWidget also provides a number of utility methods that will be
45 ConsoleWidget also provides a number of utility methods that will be
46 convenient to implementors of a console-style widget.
46 convenient to implementors of a console-style widget.
47 """
47 """
48
48
49 # Whether to process ANSI escape codes.
49 # Whether to process ANSI escape codes.
50 ansi_codes = True
50 ansi_codes = True
51
51
52 # The maximum number of lines of text before truncation.
52 # The maximum number of lines of text before truncation.
53 buffer_size = 500
53 buffer_size = 500
54
54
55 # Whether to use a list widget or plain text output for tab completion.
55 # Whether to use a list widget or plain text output for tab completion.
56 gui_completion = True
56 gui_completion = True
57
57
58 # Whether to override ShortcutEvents for the keybindings defined by this
58 # Whether to override ShortcutEvents for the keybindings defined by this
59 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
59 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
60 # priority (when it has focus) over, e.g., window-level menu shortcuts.
60 # priority (when it has focus) over, e.g., window-level menu shortcuts.
61 override_shortcuts = False
61 override_shortcuts = False
62
62
63 # Signals that indicate ConsoleWidget state.
63 # Signals that indicate ConsoleWidget state.
64 copy_available = QtCore.pyqtSignal(bool)
64 copy_available = QtCore.pyqtSignal(bool)
65 redo_available = QtCore.pyqtSignal(bool)
65 redo_available = QtCore.pyqtSignal(bool)
66 undo_available = QtCore.pyqtSignal(bool)
66 undo_available = QtCore.pyqtSignal(bool)
67
67
68 # Signal emitted when paging is needed and the paging style has been
68 # Signal emitted when paging is needed and the paging style has been
69 # specified as 'custom'.
69 # specified as 'custom'.
70 custom_page_requested = QtCore.pyqtSignal(object)
70 custom_page_requested = QtCore.pyqtSignal(object)
71
71
72 # Protected class variables.
72 # Protected class variables.
73 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
73 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
74 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
74 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
75 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
75 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
76 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
76 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
77 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
77 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
78 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
78 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
79 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
79 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
80 _shortcuts = set(_ctrl_down_remap.keys() +
80 _shortcuts = set(_ctrl_down_remap.keys() +
81 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
81 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
82
82
83 #---------------------------------------------------------------------------
83 #---------------------------------------------------------------------------
84 # 'QObject' interface
84 # 'QObject' interface
85 #---------------------------------------------------------------------------
85 #---------------------------------------------------------------------------
86
86
87 def __init__(self, kind='plain', paging='inside', parent=None):
87 def __init__(self, kind='plain', paging='inside', parent=None):
88 """ Create a ConsoleWidget.
88 """ Create a ConsoleWidget.
89
89
90 Parameters
90 Parameters
91 ----------
91 ----------
92 kind : str, optional [default 'plain']
92 kind : str, optional [default 'plain']
93 The type of underlying text widget to use. Valid values are 'plain',
93 The type of underlying text widget to use. Valid values are 'plain',
94 which specifies a QPlainTextEdit, and 'rich', which specifies a
94 which specifies a QPlainTextEdit, and 'rich', which specifies a
95 QTextEdit.
95 QTextEdit.
96
96
97 paging : str, optional [default 'inside']
97 paging : str, optional [default 'inside']
98 The type of paging to use. Valid values are:
98 The type of paging to use. Valid values are:
99 'inside' : The widget pages like a traditional terminal pager.
99 'inside' : The widget pages like a traditional terminal pager.
100 'hsplit' : When paging is requested, the widget is split
100 'hsplit' : When paging is requested, the widget is split
101 horizontally. The top pane contains the console,
101 horizontally. The top pane contains the console,
102 and the bottom pane contains the paged text.
102 and the bottom pane contains the paged text.
103 'vsplit' : Similar to 'hsplit', except that a vertical splitter
103 'vsplit' : Similar to 'hsplit', except that a vertical splitter
104 used.
104 used.
105 'custom' : No action is taken by the widget beyond emitting a
105 'custom' : No action is taken by the widget beyond emitting a
106 'custom_page_requested(str)' signal.
106 'custom_page_requested(str)' signal.
107 'none' : The text is written directly to the console.
107 'none' : The text is written directly to the console.
108
108
109 parent : QWidget, optional [default None]
109 parent : QWidget, optional [default None]
110 The parent for this widget.
110 The parent for this widget.
111 """
111 """
112 super(ConsoleWidget, self).__init__(parent)
112 super(ConsoleWidget, self).__init__(parent)
113
113
114 # Create the layout and underlying text widget.
114 # Create the layout and underlying text widget.
115 layout = QtGui.QStackedLayout(self)
115 layout = QtGui.QStackedLayout(self)
116 layout.setMargin(0)
116 layout.setMargin(0)
117 self._control = self._create_control(kind)
117 self._control = self._create_control(kind)
118 self._page_control = None
118 self._page_control = None
119 self._splitter = None
119 self._splitter = None
120 if paging in ('hsplit', 'vsplit'):
120 if paging in ('hsplit', 'vsplit'):
121 self._splitter = QtGui.QSplitter()
121 self._splitter = QtGui.QSplitter()
122 if paging == 'hsplit':
122 if paging == 'hsplit':
123 self._splitter.setOrientation(QtCore.Qt.Horizontal)
123 self._splitter.setOrientation(QtCore.Qt.Horizontal)
124 else:
124 else:
125 self._splitter.setOrientation(QtCore.Qt.Vertical)
125 self._splitter.setOrientation(QtCore.Qt.Vertical)
126 self._splitter.addWidget(self._control)
126 self._splitter.addWidget(self._control)
127 layout.addWidget(self._splitter)
127 layout.addWidget(self._splitter)
128 else:
128 else:
129 layout.addWidget(self._control)
129 layout.addWidget(self._control)
130
130
131 # Create the paging widget, if necessary.
131 # Create the paging widget, if necessary.
132 self._page_style = paging
132 self._page_style = paging
133 if paging in ('inside', 'hsplit', 'vsplit'):
133 if paging in ('inside', 'hsplit', 'vsplit'):
134 self._page_control = self._create_page_control()
134 self._page_control = self._create_page_control()
135 if self._splitter:
135 if self._splitter:
136 self._page_control.hide()
136 self._page_control.hide()
137 self._splitter.addWidget(self._page_control)
137 self._splitter.addWidget(self._page_control)
138 else:
138 else:
139 layout.addWidget(self._page_control)
139 layout.addWidget(self._page_control)
140 elif paging not in ('custom', 'none'):
140 elif paging not in ('custom', 'none'):
141 raise ValueError('Paging style %s unknown.' % repr(paging))
141 raise ValueError('Paging style %s unknown.' % repr(paging))
142
142
143 # Initialize protected variables. Some variables contain useful state
143 # Initialize protected variables. Some variables contain useful state
144 # information for subclasses; they should be considered read-only.
144 # information for subclasses; they should be considered read-only.
145 self._ansi_processor = QtAnsiCodeProcessor()
145 self._ansi_processor = QtAnsiCodeProcessor()
146 self._completion_widget = CompletionWidget(self._control)
146 self._completion_widget = CompletionWidget(self._control)
147 self._continuation_prompt = '> '
147 self._continuation_prompt = '> '
148 self._continuation_prompt_html = None
148 self._continuation_prompt_html = None
149 self._executing = False
149 self._executing = False
150 self._prompt = ''
150 self._prompt = ''
151 self._prompt_html = None
151 self._prompt_html = None
152 self._prompt_pos = 0
152 self._prompt_pos = 0
153 self._prompt_sep = ''
153 self._prompt_sep = ''
154 self._reading = False
154 self._reading = False
155 self._reading_callback = None
155 self._reading_callback = None
156 self._tab_width = 8
156 self._tab_width = 8
157
157
158 # Set a monospaced font.
158 # Set a monospaced font.
159 self.reset_font()
159 self.reset_font()
160
160
161 def eventFilter(self, obj, event):
161 def eventFilter(self, obj, event):
162 """ Reimplemented to ensure a console-like behavior in the underlying
162 """ Reimplemented to ensure a console-like behavior in the underlying
163 text widget.
163 text widget.
164 """
164 """
165 # Re-map keys for all filtered widgets.
165 # Re-map keys for all filtered widgets.
166 etype = event.type()
166 etype = event.type()
167 if etype == QtCore.QEvent.KeyPress and \
167 if etype == QtCore.QEvent.KeyPress and \
168 self._control_key_down(event.modifiers()) and \
168 self._control_key_down(event.modifiers()) and \
169 event.key() in self._ctrl_down_remap:
169 event.key() in self._ctrl_down_remap:
170 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
170 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
171 self._ctrl_down_remap[event.key()],
171 self._ctrl_down_remap[event.key()],
172 QtCore.Qt.NoModifier)
172 QtCore.Qt.NoModifier)
173 QtGui.qApp.sendEvent(obj, new_event)
173 QtGui.qApp.sendEvent(obj, new_event)
174 return True
174 return True
175
175
176 # Override shortucts for all filtered widgets. Note that on Mac OS it is
176 # Override shortucts for all filtered widgets. Note that on Mac OS it is
177 # always unnecessary to override shortcuts, hence the check below (users
177 # always unnecessary to override shortcuts, hence the check below (users
178 # should just use the Control key instead of the Command key).
178 # should just use the Control key instead of the Command key).
179 elif etype == QtCore.QEvent.ShortcutOverride and \
179 elif etype == QtCore.QEvent.ShortcutOverride and \
180 sys.platform != 'darwin' and \
180 sys.platform != 'darwin' and \
181 self._control_key_down(event.modifiers()) and \
181 self._control_key_down(event.modifiers()) and \
182 event.key() in self._shortcuts:
182 event.key() in self._shortcuts:
183 event.accept()
183 event.accept()
184 return False
184 return False
185
185
186 elif etype == QtCore.QEvent.KeyPress:
186 elif etype == QtCore.QEvent.KeyPress:
187 if obj == self._control:
187 if obj == self._control:
188 return self._event_filter_console_keypress(event)
188 return self._event_filter_console_keypress(event)
189 elif obj == self._page_control:
189 elif obj == self._page_control:
190 return self._event_filter_page_keypress(event)
190 return self._event_filter_page_keypress(event)
191
191
192 return super(ConsoleWidget, self).eventFilter(obj, event)
192 return super(ConsoleWidget, self).eventFilter(obj, event)
193
193
194 #---------------------------------------------------------------------------
194 #---------------------------------------------------------------------------
195 # 'QWidget' interface
195 # 'QWidget' interface
196 #---------------------------------------------------------------------------
196 #---------------------------------------------------------------------------
197
197
198 def sizeHint(self):
198 def sizeHint(self):
199 """ Reimplemented to suggest a size that is 80 characters wide and
199 """ Reimplemented to suggest a size that is 80 characters wide and
200 25 lines high.
200 25 lines high.
201 """
201 """
202 style = self.style()
202 style = self.style()
203 opt = QtGui.QStyleOptionHeader()
203 opt = QtGui.QStyleOptionHeader()
204 font_metrics = QtGui.QFontMetrics(self.font)
204 font_metrics = QtGui.QFontMetrics(self.font)
205 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
205 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
206
206
207 width = font_metrics.width(' ') * 80
207 width = font_metrics.width(' ') * 80
208 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
208 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
209 if self._page_style == 'hsplit':
209 if self._page_style == 'hsplit':
210 width = width * 2 + splitwidth
210 width = width * 2 + splitwidth
211
211
212 height = font_metrics.height() * 25
212 height = font_metrics.height() * 25
213 if self._page_style == 'vsplit':
213 if self._page_style == 'vsplit':
214 height = height * 2 + splitwidth
214 height = height * 2 + splitwidth
215
215
216 return QtCore.QSize(width, height)
216 return QtCore.QSize(width, height)
217
217
218 #---------------------------------------------------------------------------
218 #---------------------------------------------------------------------------
219 # 'ConsoleWidget' public interface
219 # 'ConsoleWidget' public interface
220 #---------------------------------------------------------------------------
220 #---------------------------------------------------------------------------
221
221
222 def can_paste(self):
222 def can_paste(self):
223 """ Returns whether text can be pasted from the clipboard.
223 """ Returns whether text can be pasted from the clipboard.
224 """
224 """
225 # Accept only text that can be ASCII encoded.
225 # Accept only text that can be ASCII encoded.
226 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
226 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
227 text = QtGui.QApplication.clipboard().text()
227 text = QtGui.QApplication.clipboard().text()
228 if not text.isEmpty():
228 if not text.isEmpty():
229 try:
229 try:
230 str(text)
230 str(text)
231 return True
231 return True
232 except UnicodeEncodeError:
232 except UnicodeEncodeError:
233 pass
233 pass
234 return False
234 return False
235
235
236 def clear(self, keep_input=True):
236 def clear(self, keep_input=True):
237 """ Clear the console, then write a new prompt. If 'keep_input' is set,
237 """ Clear the console, then write a new prompt. If 'keep_input' is set,
238 restores the old input buffer when the new prompt is written.
238 restores the old input buffer when the new prompt is written.
239 """
239 """
240 if keep_input:
240 if keep_input:
241 input_buffer = self.input_buffer
241 input_buffer = self.input_buffer
242 self._control.clear()
242 self._control.clear()
243 self._show_prompt()
243 self._show_prompt()
244 if keep_input:
244 if keep_input:
245 self.input_buffer = input_buffer
245 self.input_buffer = input_buffer
246
246
247 def copy(self):
247 def copy(self):
248 """ Copy the current selected text to the clipboard.
248 """ Copy the current selected text to the clipboard.
249 """
249 """
250 self._control.copy()
250 self._control.copy()
251
251
252 def execute(self, source=None, hidden=False, interactive=False):
252 def execute(self, source=None, hidden=False, interactive=False):
253 """ Executes source or the input buffer, possibly prompting for more
253 """ Executes source or the input buffer, possibly prompting for more
254 input.
254 input.
255
255
256 Parameters:
256 Parameters:
257 -----------
257 -----------
258 source : str, optional
258 source : str, optional
259
259
260 The source to execute. If not specified, the input buffer will be
260 The source to execute. If not specified, the input buffer will be
261 used. If specified and 'hidden' is False, the input buffer will be
261 used. If specified and 'hidden' is False, the input buffer will be
262 replaced with the source before execution.
262 replaced with the source before execution.
263
263
264 hidden : bool, optional (default False)
264 hidden : bool, optional (default False)
265
265
266 If set, no output will be shown and the prompt will not be modified.
266 If set, no output will be shown and the prompt will not be modified.
267 In other words, it will be completely invisible to the user that
267 In other words, it will be completely invisible to the user that
268 an execution has occurred.
268 an execution has occurred.
269
269
270 interactive : bool, optional (default False)
270 interactive : bool, optional (default False)
271
271
272 Whether the console is to treat the source as having been manually
272 Whether the console is to treat the source as having been manually
273 entered by the user. The effect of this parameter depends on the
273 entered by the user. The effect of this parameter depends on the
274 subclass implementation.
274 subclass implementation.
275
275
276 Raises:
276 Raises:
277 -------
277 -------
278 RuntimeError
278 RuntimeError
279 If incomplete input is given and 'hidden' is True. In this case,
279 If incomplete input is given and 'hidden' is True. In this case,
280 it is not possible to prompt for more input.
280 it is not possible to prompt for more input.
281
281
282 Returns:
282 Returns:
283 --------
283 --------
284 A boolean indicating whether the source was executed.
284 A boolean indicating whether the source was executed.
285 """
285 """
286 # WARNING: The order in which things happen here is very particular, in
286 # WARNING: The order in which things happen here is very particular, in
287 # large part because our syntax highlighting is fragile. If you change
287 # large part because our syntax highlighting is fragile. If you change
288 # something, test carefully!
288 # something, test carefully!
289
289
290 # Decide what to execute.
290 # Decide what to execute.
291 if source is None:
291 if source is None:
292 source = self.input_buffer
292 source = self.input_buffer
293 if not hidden:
293 if not hidden:
294 # A newline is appended later, but it should be considered part
294 # A newline is appended later, but it should be considered part
295 # of the input buffer.
295 # of the input buffer.
296 source += '\n'
296 source += '\n'
297 elif not hidden:
297 elif not hidden:
298 self.input_buffer = source
298 self.input_buffer = source
299
299
300 # Execute the source or show a continuation prompt if it is incomplete.
300 # Execute the source or show a continuation prompt if it is incomplete.
301 complete = self._is_complete(source, interactive)
301 complete = self._is_complete(source, interactive)
302 if hidden:
302 if hidden:
303 if complete:
303 if complete:
304 self._execute(source, hidden)
304 self._execute(source, hidden)
305 else:
305 else:
306 error = 'Incomplete noninteractive input: "%s"'
306 error = 'Incomplete noninteractive input: "%s"'
307 raise RuntimeError(error % source)
307 raise RuntimeError(error % source)
308 else:
308 else:
309 if complete:
309 if complete:
310 self._append_plain_text('\n')
310 self._append_plain_text('\n')
311 self._executing_input_buffer = self.input_buffer
311 self._executing_input_buffer = self.input_buffer
312 self._executing = True
312 self._executing = True
313 self._prompt_finished()
313 self._prompt_finished()
314
314
315 # The maximum block count is only in effect during execution.
315 # The maximum block count is only in effect during execution.
316 # This ensures that _prompt_pos does not become invalid due to
316 # This ensures that _prompt_pos does not become invalid due to
317 # text truncation.
317 # text truncation.
318 self._control.document().setMaximumBlockCount(self.buffer_size)
318 self._control.document().setMaximumBlockCount(self.buffer_size)
319
319
320 # Setting a positive maximum block count will automatically
320 # Setting a positive maximum block count will automatically
321 # disable the undo/redo history, but just to be safe:
321 # disable the undo/redo history, but just to be safe:
322 self._control.setUndoRedoEnabled(False)
322 self._control.setUndoRedoEnabled(False)
323
323
324 self._execute(source, hidden)
324 self._execute(source, hidden)
325
325
326 else:
326 else:
327 # Do this inside an edit block so continuation prompts are
327 # Do this inside an edit block so continuation prompts are
328 # removed seamlessly via undo/redo.
328 # removed seamlessly via undo/redo.
329 cursor = self._control.textCursor()
329 cursor = self._control.textCursor()
330 cursor.beginEditBlock()
330 cursor.beginEditBlock()
331
331
332 self._append_plain_text('\n')
332 self._append_plain_text('\n')
333 self._show_continuation_prompt()
333 self._show_continuation_prompt()
334
334
335 cursor.endEditBlock()
335 cursor.endEditBlock()
336
336
337 return complete
337 return complete
338
338
339 def _get_input_buffer(self):
339 def _get_input_buffer(self):
340 """ The text that the user has entered entered at the current prompt.
340 """ The text that the user has entered entered at the current prompt.
341 """
341 """
342 # If we're executing, the input buffer may not even exist anymore due to
342 # If we're executing, the input buffer may not even exist anymore due to
343 # the limit imposed by 'buffer_size'. Therefore, we store it.
343 # the limit imposed by 'buffer_size'. Therefore, we store it.
344 if self._executing:
344 if self._executing:
345 return self._executing_input_buffer
345 return self._executing_input_buffer
346
346
347 cursor = self._get_end_cursor()
347 cursor = self._get_end_cursor()
348 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
348 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
349 input_buffer = str(cursor.selection().toPlainText())
349 input_buffer = str(cursor.selection().toPlainText())
350
350
351 # Strip out continuation prompts.
351 # Strip out continuation prompts.
352 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
352 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
353
353
354 def _set_input_buffer(self, string):
354 def _set_input_buffer(self, string):
355 """ Replaces the text in the input buffer with 'string'.
355 """ Replaces the text in the input buffer with 'string'.
356 """
356 """
357 # For now, it is an error to modify the input buffer during execution.
357 # For now, it is an error to modify the input buffer during execution.
358 if self._executing:
358 if self._executing:
359 raise RuntimeError("Cannot change input buffer during execution.")
359 raise RuntimeError("Cannot change input buffer during execution.")
360
360
361 # Remove old text.
361 # Remove old text.
362 cursor = self._get_end_cursor()
362 cursor = self._get_end_cursor()
363 cursor.beginEditBlock()
363 cursor.beginEditBlock()
364 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
364 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
365 cursor.removeSelectedText()
365 cursor.removeSelectedText()
366
366
367 # Insert new text with continuation prompts.
367 # Insert new text with continuation prompts.
368 lines = string.splitlines(True)
368 lines = string.splitlines(True)
369 if lines:
369 if lines:
370 self._append_plain_text(lines[0])
370 self._append_plain_text(lines[0])
371 for i in xrange(1, len(lines)):
371 for i in xrange(1, len(lines)):
372 if self._continuation_prompt_html is None:
372 if self._continuation_prompt_html is None:
373 self._append_plain_text(self._continuation_prompt)
373 self._append_plain_text(self._continuation_prompt)
374 else:
374 else:
375 self._append_html(self._continuation_prompt_html)
375 self._append_html(self._continuation_prompt_html)
376 self._append_plain_text(lines[i])
376 self._append_plain_text(lines[i])
377 cursor.endEditBlock()
377 cursor.endEditBlock()
378 self._control.moveCursor(QtGui.QTextCursor.End)
378 self._control.moveCursor(QtGui.QTextCursor.End)
379
379
380 input_buffer = property(_get_input_buffer, _set_input_buffer)
380 input_buffer = property(_get_input_buffer, _set_input_buffer)
381
381
382 def _get_font(self):
382 def _get_font(self):
383 """ The base font being used by the ConsoleWidget.
383 """ The base font being used by the ConsoleWidget.
384 """
384 """
385 return self._control.document().defaultFont()
385 return self._control.document().defaultFont()
386
386
387 def _set_font(self, font):
387 def _set_font(self, font):
388 """ Sets the base font for the ConsoleWidget to the specified QFont.
388 """ Sets the base font for the ConsoleWidget to the specified QFont.
389 """
389 """
390 font_metrics = QtGui.QFontMetrics(font)
390 font_metrics = QtGui.QFontMetrics(font)
391 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
391 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
392
392
393 self._completion_widget.setFont(font)
393 self._completion_widget.setFont(font)
394 self._control.document().setDefaultFont(font)
394 self._control.document().setDefaultFont(font)
395 if self._page_control:
395 if self._page_control:
396 self._page_control.document().setDefaultFont(font)
396 self._page_control.document().setDefaultFont(font)
397
397
398 font = property(_get_font, _set_font)
398 font = property(_get_font, _set_font)
399
399
400 def paste(self):
400 def paste(self):
401 """ Paste the contents of the clipboard into the input region.
401 """ Paste the contents of the clipboard into the input region.
402 """
402 """
403 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
403 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
404 try:
404 try:
405 text = str(QtGui.QApplication.clipboard().text())
405 text = str(QtGui.QApplication.clipboard().text())
406 except UnicodeEncodeError:
406 except UnicodeEncodeError:
407 pass
407 pass
408 else:
408 else:
409 self._insert_plain_text_into_buffer(dedent(text))
409 self._insert_plain_text_into_buffer(dedent(text))
410
410
411 def print_(self, printer):
411 def print_(self, printer):
412 """ Print the contents of the ConsoleWidget to the specified QPrinter.
412 """ Print the contents of the ConsoleWidget to the specified QPrinter.
413 """
413 """
414 self._control.print_(printer)
414 self._control.print_(printer)
415
415
416 def redo(self):
416 def redo(self):
417 """ Redo the last operation. If there is no operation to redo, nothing
417 """ Redo the last operation. If there is no operation to redo, nothing
418 happens.
418 happens.
419 """
419 """
420 self._control.redo()
420 self._control.redo()
421
421
422 def reset_font(self):
422 def reset_font(self):
423 """ Sets the font to the default fixed-width font for this platform.
423 """ Sets the font to the default fixed-width font for this platform.
424 """
424 """
425 # FIXME: font family and size should be configurable by the user.
425 # FIXME: font family and size should be configurable by the user.
426
426
427 if sys.platform == 'win32':
427 if sys.platform == 'win32':
428 # Fixme: we should test whether Consolas is available and use it
428 # Fixme: we should test whether Consolas is available and use it
429 # first if it is. Consolas ships by default from Vista onwards,
429 # first if it is. Consolas ships by default from Vista onwards,
430 # it's *vastly* more readable and prettier than Courier, and is
430 # it's *vastly* more readable and prettier than Courier, and is
431 # often installed even on XP systems. So we should first check for
431 # often installed even on XP systems. So we should first check for
432 # it, and only fallback to Courier if absolutely necessary.
432 # it, and only fallback to Courier if absolutely necessary.
433 name = 'Courier'
433 name = 'Courier'
434 elif sys.platform == 'darwin':
434 elif sys.platform == 'darwin':
435 name = 'Monaco'
435 name = 'Monaco'
436 else:
436 else:
437 name = 'Monospace'
437 name = 'Monospace'
438 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
438 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
439 font.setStyleHint(QtGui.QFont.TypeWriter)
439 font.setStyleHint(QtGui.QFont.TypeWriter)
440 self._set_font(font)
440 self._set_font(font)
441
441
442 def select_all(self):
442 def select_all(self):
443 """ Selects all the text in the buffer.
443 """ Selects all the text in the buffer.
444 """
444 """
445 self._control.selectAll()
445 self._control.selectAll()
446
446
447 def _get_tab_width(self):
447 def _get_tab_width(self):
448 """ The width (in terms of space characters) for tab characters.
448 """ The width (in terms of space characters) for tab characters.
449 """
449 """
450 return self._tab_width
450 return self._tab_width
451
451
452 def _set_tab_width(self, tab_width):
452 def _set_tab_width(self, tab_width):
453 """ Sets the width (in terms of space characters) for tab characters.
453 """ Sets the width (in terms of space characters) for tab characters.
454 """
454 """
455 font_metrics = QtGui.QFontMetrics(self.font)
455 font_metrics = QtGui.QFontMetrics(self.font)
456 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
456 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
457
457
458 self._tab_width = tab_width
458 self._tab_width = tab_width
459
459
460 tab_width = property(_get_tab_width, _set_tab_width)
460 tab_width = property(_get_tab_width, _set_tab_width)
461
461
462 def undo(self):
462 def undo(self):
463 """ Undo the last operation. If there is no operation to undo, nothing
463 """ Undo the last operation. If there is no operation to undo, nothing
464 happens.
464 happens.
465 """
465 """
466 self._control.undo()
466 self._control.undo()
467
467
468 #---------------------------------------------------------------------------
468 #---------------------------------------------------------------------------
469 # 'ConsoleWidget' abstract interface
469 # 'ConsoleWidget' abstract interface
470 #---------------------------------------------------------------------------
470 #---------------------------------------------------------------------------
471
471
472 def _is_complete(self, source, interactive):
472 def _is_complete(self, source, interactive):
473 """ Returns whether 'source' can be executed. When triggered by an
473 """ Returns whether 'source' can be executed. When triggered by an
474 Enter/Return key press, 'interactive' is True; otherwise, it is
474 Enter/Return key press, 'interactive' is True; otherwise, it is
475 False.
475 False.
476 """
476 """
477 raise NotImplementedError
477 raise NotImplementedError
478
478
479 def _execute(self, source, hidden):
479 def _execute(self, source, hidden):
480 """ Execute 'source'. If 'hidden', do not show any output.
480 """ Execute 'source'. If 'hidden', do not show any output.
481 """
481 """
482 raise NotImplementedError
482 raise NotImplementedError
483
483
484 def _prompt_started_hook(self):
484 def _prompt_started_hook(self):
485 """ Called immediately after a new prompt is displayed.
485 """ Called immediately after a new prompt is displayed.
486 """
486 """
487 pass
487 pass
488
488
489 def _prompt_finished_hook(self):
489 def _prompt_finished_hook(self):
490 """ Called immediately after a prompt is finished, i.e. when some input
490 """ Called immediately after a prompt is finished, i.e. when some input
491 will be processed and a new prompt displayed.
491 will be processed and a new prompt displayed.
492 """
492 """
493 pass
493 pass
494
494
495 def _up_pressed(self):
495 def _up_pressed(self):
496 """ Called when the up key is pressed. Returns whether to continue
496 """ Called when the up key is pressed. Returns whether to continue
497 processing the event.
497 processing the event.
498 """
498 """
499 return True
499 return True
500
500
501 def _down_pressed(self):
501 def _down_pressed(self):
502 """ Called when the down key is pressed. Returns whether to continue
502 """ Called when the down key is pressed. Returns whether to continue
503 processing the event.
503 processing the event.
504 """
504 """
505 return True
505 return True
506
506
507 def _tab_pressed(self):
507 def _tab_pressed(self):
508 """ Called when the tab key is pressed. Returns whether to continue
508 """ Called when the tab key is pressed. Returns whether to continue
509 processing the event.
509 processing the event.
510 """
510 """
511 return False
511 return False
512
512
513 #--------------------------------------------------------------------------
513 #--------------------------------------------------------------------------
514 # 'ConsoleWidget' protected interface
514 # 'ConsoleWidget' protected interface
515 #--------------------------------------------------------------------------
515 #--------------------------------------------------------------------------
516
516
517 def _append_html(self, html):
517 def _append_html(self, html):
518 """ Appends html at the end of the console buffer.
518 """ Appends html at the end of the console buffer.
519 """
519 """
520 cursor = self._get_end_cursor()
520 cursor = self._get_end_cursor()
521 self._insert_html(cursor, html)
521 self._insert_html(cursor, html)
522
522
523 def _append_html_fetching_plain_text(self, html):
523 def _append_html_fetching_plain_text(self, html):
524 """ Appends 'html', then returns the plain text version of it.
524 """ Appends 'html', then returns the plain text version of it.
525 """
525 """
526 cursor = self._get_end_cursor()
526 cursor = self._get_end_cursor()
527 return self._insert_html_fetching_plain_text(cursor, html)
527 return self._insert_html_fetching_plain_text(cursor, html)
528
528
529 def _append_plain_text(self, text):
529 def _append_plain_text(self, text):
530 """ Appends plain text at the end of the console buffer, processing
530 """ Appends plain text at the end of the console buffer, processing
531 ANSI codes if enabled.
531 ANSI codes if enabled.
532 """
532 """
533 cursor = self._get_end_cursor()
533 cursor = self._get_end_cursor()
534 self._insert_plain_text(cursor, text)
534 self._insert_plain_text(cursor, text)
535
535
536 def _append_plain_text_keeping_prompt(self, text):
536 def _append_plain_text_keeping_prompt(self, text):
537 """ Writes 'text' after the current prompt, then restores the old prompt
537 """ Writes 'text' after the current prompt, then restores the old prompt
538 with its old input buffer.
538 with its old input buffer.
539 """
539 """
540 input_buffer = self.input_buffer
540 input_buffer = self.input_buffer
541 self._append_plain_text('\n')
541 self._append_plain_text('\n')
542 self._prompt_finished()
542 self._prompt_finished()
543
543
544 self._append_plain_text(text)
544 self._append_plain_text(text)
545 self._show_prompt()
545 self._show_prompt()
546 self.input_buffer = input_buffer
546 self.input_buffer = input_buffer
547
547
548 def _complete_with_items(self, cursor, items):
548 def _complete_with_items(self, cursor, items):
549 """ Performs completion with 'items' at the specified cursor location.
549 """ Performs completion with 'items' at the specified cursor location.
550 """
550 """
551 if len(items) == 1:
551 if len(items) == 1:
552 cursor.setPosition(self._control.textCursor().position(),
552 cursor.setPosition(self._control.textCursor().position(),
553 QtGui.QTextCursor.KeepAnchor)
553 QtGui.QTextCursor.KeepAnchor)
554 cursor.insertText(items[0])
554 cursor.insertText(items[0])
555 elif len(items) > 1:
555 elif len(items) > 1:
556 if self.gui_completion:
556 if self.gui_completion:
557 self._completion_widget.show_items(cursor, items)
557 self._completion_widget.show_items(cursor, items)
558 else:
558 else:
559 text = self._format_as_columns(items)
559 text = self._format_as_columns(items)
560 self._append_plain_text_keeping_prompt(text)
560 self._append_plain_text_keeping_prompt(text)
561
561
562 def _control_key_down(self, modifiers):
562 def _control_key_down(self, modifiers):
563 """ Given a KeyboardModifiers flags object, return whether the Control
563 """ Given a KeyboardModifiers flags object, return whether the Control
564 key is down (on Mac OS, treat the Command key as a synonym for
564 key is down (on Mac OS, treat the Command key as a synonym for
565 Control).
565 Control).
566 """
566 """
567 down = bool(modifiers & QtCore.Qt.ControlModifier)
567 down = bool(modifiers & QtCore.Qt.ControlModifier)
568
568
569 # Note: on Mac OS, ControlModifier corresponds to the Command key while
569 # Note: on Mac OS, ControlModifier corresponds to the Command key while
570 # MetaModifier corresponds to the Control key.
570 # MetaModifier corresponds to the Control key.
571 if sys.platform == 'darwin':
571 if sys.platform == 'darwin':
572 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
572 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
573
573
574 return down
574 return down
575
575
576 def _create_control(self, kind):
576 def _create_control(self, kind):
577 """ Creates and connects the underlying text widget.
577 """ Creates and connects the underlying text widget.
578 """
578 """
579 if kind == 'plain':
579 if kind == 'plain':
580 control = ConsolePlainTextEdit()
580 control = ConsolePlainTextEdit()
581 elif kind == 'rich':
581 elif kind == 'rich':
582 control = ConsoleTextEdit()
582 control = ConsoleTextEdit()
583 control.setAcceptRichText(False)
583 control.setAcceptRichText(False)
584 else:
584 else:
585 raise ValueError("Kind %s unknown." % repr(kind))
585 raise ValueError("Kind %s unknown." % repr(kind))
586 control.installEventFilter(self)
586 control.installEventFilter(self)
587 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
587 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
588 control.customContextMenuRequested.connect(self._show_context_menu)
588 control.customContextMenuRequested.connect(self._show_context_menu)
589 control.copyAvailable.connect(self.copy_available)
589 control.copyAvailable.connect(self.copy_available)
590 control.redoAvailable.connect(self.redo_available)
590 control.redoAvailable.connect(self.redo_available)
591 control.undoAvailable.connect(self.undo_available)
591 control.undoAvailable.connect(self.undo_available)
592 control.setReadOnly(True)
592 control.setReadOnly(True)
593 control.setUndoRedoEnabled(False)
593 control.setUndoRedoEnabled(False)
594 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
594 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
595 return control
595 return control
596
596
597 def _create_page_control(self):
597 def _create_page_control(self):
598 """ Creates and connects the underlying paging widget.
598 """ Creates and connects the underlying paging widget.
599 """
599 """
600 control = ConsolePlainTextEdit()
600 control = ConsolePlainTextEdit()
601 control.installEventFilter(self)
601 control.installEventFilter(self)
602 control.setReadOnly(True)
602 control.setReadOnly(True)
603 control.setUndoRedoEnabled(False)
603 control.setUndoRedoEnabled(False)
604 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
604 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
605 return control
605 return control
606
606
607 def _event_filter_console_keypress(self, event):
607 def _event_filter_console_keypress(self, event):
608 """ Filter key events for the underlying text widget to create a
608 """ Filter key events for the underlying text widget to create a
609 console-like interface.
609 console-like interface.
610 """
610 """
611 intercepted = False
611 intercepted = False
612 cursor = self._control.textCursor()
612 cursor = self._control.textCursor()
613 position = cursor.position()
613 position = cursor.position()
614 key = event.key()
614 key = event.key()
615 ctrl_down = self._control_key_down(event.modifiers())
615 ctrl_down = self._control_key_down(event.modifiers())
616 alt_down = event.modifiers() & QtCore.Qt.AltModifier
616 alt_down = event.modifiers() & QtCore.Qt.AltModifier
617 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
617 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
618
618
619 if event.matches(QtGui.QKeySequence.Paste):
619 if event.matches(QtGui.QKeySequence.Paste):
620 # Call our paste instead of the underlying text widget's.
620 # Call our paste instead of the underlying text widget's.
621 self.paste()
621 self.paste()
622 intercepted = True
622 intercepted = True
623
623
624 elif ctrl_down:
624 elif ctrl_down:
625 if key == QtCore.Qt.Key_K:
625 if key == QtCore.Qt.Key_K:
626 if self._in_buffer(position):
626 if self._in_buffer(position):
627 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
627 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
628 QtGui.QTextCursor.KeepAnchor)
628 QtGui.QTextCursor.KeepAnchor)
629 cursor.removeSelectedText()
629 cursor.removeSelectedText()
630 intercepted = True
630 intercepted = True
631
631
632 elif key == QtCore.Qt.Key_X:
632 elif key == QtCore.Qt.Key_X:
633 intercepted = True
633 intercepted = True
634
634
635 elif key == QtCore.Qt.Key_Y:
635 elif key == QtCore.Qt.Key_Y:
636 self.paste()
636 self.paste()
637 intercepted = True
637 intercepted = True
638
638
639 elif alt_down:
639 elif alt_down:
640 if key == QtCore.Qt.Key_B:
640 if key == QtCore.Qt.Key_B:
641 self._set_cursor(self._get_word_start_cursor(position))
641 self._set_cursor(self._get_word_start_cursor(position))
642 intercepted = True
642 intercepted = True
643
643
644 elif key == QtCore.Qt.Key_F:
644 elif key == QtCore.Qt.Key_F:
645 self._set_cursor(self._get_word_end_cursor(position))
645 self._set_cursor(self._get_word_end_cursor(position))
646 intercepted = True
646 intercepted = True
647
647
648 elif key == QtCore.Qt.Key_Backspace:
648 elif key == QtCore.Qt.Key_Backspace:
649 cursor = self._get_word_start_cursor(position)
649 cursor = self._get_word_start_cursor(position)
650 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
650 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
651 cursor.removeSelectedText()
651 cursor.removeSelectedText()
652 intercepted = True
652 intercepted = True
653
653
654 elif key == QtCore.Qt.Key_D:
654 elif key == QtCore.Qt.Key_D:
655 cursor = self._get_word_end_cursor(position)
655 cursor = self._get_word_end_cursor(position)
656 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
656 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
657 cursor.removeSelectedText()
657 cursor.removeSelectedText()
658 intercepted = True
658 intercepted = True
659
659
660 else:
660 else:
661 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
661 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
662 if self._reading:
662 if self._reading:
663 self._append_plain_text('\n')
663 self._append_plain_text('\n')
664 self._reading = False
664 self._reading = False
665 if self._reading_callback:
665 if self._reading_callback:
666 self._reading_callback()
666 self._reading_callback()
667 elif not self._executing:
667 elif not self._executing:
668 self.execute(interactive=True)
668 self.execute(interactive=True)
669 intercepted = True
669 intercepted = True
670
670
671 elif key == QtCore.Qt.Key_Up:
671 elif key == QtCore.Qt.Key_Up:
672 if self._reading or not self._up_pressed():
672 if self._reading or not self._up_pressed():
673 intercepted = True
673 intercepted = True
674 else:
674 else:
675 prompt_line = self._get_prompt_cursor().blockNumber()
675 prompt_line = self._get_prompt_cursor().blockNumber()
676 intercepted = cursor.blockNumber() <= prompt_line
676 intercepted = cursor.blockNumber() <= prompt_line
677
677
678 elif key == QtCore.Qt.Key_Down:
678 elif key == QtCore.Qt.Key_Down:
679 if self._reading or not self._down_pressed():
679 if self._reading or not self._down_pressed():
680 intercepted = True
680 intercepted = True
681 else:
681 else:
682 end_line = self._get_end_cursor().blockNumber()
682 end_line = self._get_end_cursor().blockNumber()
683 intercepted = cursor.blockNumber() == end_line
683 intercepted = cursor.blockNumber() == end_line
684
684
685 elif key == QtCore.Qt.Key_Tab:
685 elif key == QtCore.Qt.Key_Tab:
686 if not self._reading:
686 if not self._reading:
687 intercepted = not self._tab_pressed()
687 intercepted = not self._tab_pressed()
688
688
689 elif key == QtCore.Qt.Key_Left:
689 elif key == QtCore.Qt.Key_Left:
690 intercepted = not self._in_buffer(position - 1)
690 intercepted = not self._in_buffer(position - 1)
691
691
692 elif key == QtCore.Qt.Key_Home:
692 elif key == QtCore.Qt.Key_Home:
693 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
693 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
694 start_line = cursor.blockNumber()
694 start_line = cursor.blockNumber()
695 if start_line == self._get_prompt_cursor().blockNumber():
695 if start_line == self._get_prompt_cursor().blockNumber():
696 start_pos = self._prompt_pos
696 start_pos = self._prompt_pos
697 else:
697 else:
698 start_pos = cursor.position()
698 start_pos = cursor.position()
699 start_pos += len(self._continuation_prompt)
699 start_pos += len(self._continuation_prompt)
700 if shift_down and self._in_buffer(position):
700 if shift_down and self._in_buffer(position):
701 self._set_selection(position, start_pos)
701 self._set_selection(position, start_pos)
702 else:
702 else:
703 self._set_position(start_pos)
703 self._set_position(start_pos)
704 intercepted = True
704 intercepted = True
705
705
706 elif key == QtCore.Qt.Key_Backspace:
706 elif key == QtCore.Qt.Key_Backspace:
707
707
708 # Line deletion (remove continuation prompt)
708 # Line deletion (remove continuation prompt)
709 len_prompt = len(self._continuation_prompt)
709 len_prompt = len(self._continuation_prompt)
710 if not self._reading and \
710 if not self._reading and \
711 cursor.columnNumber() == len_prompt and \
711 cursor.columnNumber() == len_prompt and \
712 position != self._prompt_pos:
712 position != self._prompt_pos:
713 cursor.beginEditBlock()
713 cursor.beginEditBlock()
714 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
714 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
715 QtGui.QTextCursor.KeepAnchor)
715 QtGui.QTextCursor.KeepAnchor)
716 cursor.removeSelectedText()
716 cursor.removeSelectedText()
717 cursor.deletePreviousChar()
717 cursor.deletePreviousChar()
718 cursor.endEditBlock()
718 cursor.endEditBlock()
719 intercepted = True
719 intercepted = True
720
720
721 # Regular backwards deletion
721 # Regular backwards deletion
722 else:
722 else:
723 anchor = cursor.anchor()
723 anchor = cursor.anchor()
724 if anchor == position:
724 if anchor == position:
725 intercepted = not self._in_buffer(position - 1)
725 intercepted = not self._in_buffer(position - 1)
726 else:
726 else:
727 intercepted = not self._in_buffer(min(anchor, position))
727 intercepted = not self._in_buffer(min(anchor, position))
728
728
729 elif key == QtCore.Qt.Key_Delete:
729 elif key == QtCore.Qt.Key_Delete:
730
730
731 # Line deletion (remove continuation prompt)
731 # Line deletion (remove continuation prompt)
732 if not self._reading and cursor.atBlockEnd() and not \
732 if not self._reading and cursor.atBlockEnd() and not \
733 cursor.hasSelection():
733 cursor.hasSelection():
734 cursor.movePosition(QtGui.QTextCursor.NextBlock,
734 cursor.movePosition(QtGui.QTextCursor.NextBlock,
735 QtGui.QTextCursor.KeepAnchor)
735 QtGui.QTextCursor.KeepAnchor)
736 cursor.movePosition(QtGui.QTextCursor.Right,
736 cursor.movePosition(QtGui.QTextCursor.Right,
737 QtGui.QTextCursor.KeepAnchor,
737 QtGui.QTextCursor.KeepAnchor,
738 len(self._continuation_prompt))
738 len(self._continuation_prompt))
739 cursor.removeSelectedText()
739 cursor.removeSelectedText()
740 intercepted = True
740 intercepted = True
741
741
742 # Regular forwards deletion:
742 # Regular forwards deletion:
743 else:
743 else:
744 anchor = cursor.anchor()
744 anchor = cursor.anchor()
745 intercepted = (not self._in_buffer(anchor) or
745 intercepted = (not self._in_buffer(anchor) or
746 not self._in_buffer(position))
746 not self._in_buffer(position))
747
747
748 # Don't move the cursor if control is down to allow copy-paste using
748 # Don't move the cursor if control is down to allow copy-paste using
749 # the keyboard in any part of the buffer.
749 # the keyboard in any part of the buffer.
750 if not ctrl_down:
750 if not ctrl_down:
751 self._keep_cursor_in_buffer()
751 self._keep_cursor_in_buffer()
752
752
753 return intercepted
753 return intercepted
754
754
755 def _event_filter_page_keypress(self, event):
755 def _event_filter_page_keypress(self, event):
756 """ Filter key events for the paging widget to create console-like
756 """ Filter key events for the paging widget to create console-like
757 interface.
757 interface.
758 """
758 """
759 key = event.key()
759 key = event.key()
760
760
761 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
761 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
762 if self._splitter:
762 if self._splitter:
763 self._page_control.hide()
763 self._page_control.hide()
764 else:
764 else:
765 self.layout().setCurrentWidget(self._control)
765 self.layout().setCurrentWidget(self._control)
766 return True
766 return True
767
767
768 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
768 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
769 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
769 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
770 QtCore.Qt.Key_Down,
770 QtCore.Qt.Key_Down,
771 QtCore.Qt.NoModifier)
771 QtCore.Qt.NoModifier)
772 QtGui.qApp.sendEvent(self._page_control, new_event)
772 QtGui.qApp.sendEvent(self._page_control, new_event)
773 return True
773 return True
774
774
775 return False
775 return False
776
776
777 def _format_as_columns(self, items, separator=' '):
777 def _format_as_columns(self, items, separator=' '):
778 """ Transform a list of strings into a single string with columns.
778 """ Transform a list of strings into a single string with columns.
779
779
780 Parameters
780 Parameters
781 ----------
781 ----------
782 items : sequence of strings
782 items : sequence of strings
783 The strings to process.
783 The strings to process.
784
784
785 separator : str, optional [default is two spaces]
785 separator : str, optional [default is two spaces]
786 The string that separates columns.
786 The string that separates columns.
787
787
788 Returns
788 Returns
789 -------
789 -------
790 The formatted string.
790 The formatted string.
791 """
791 """
792 # Note: this code is adapted from columnize 0.3.2.
792 # Note: this code is adapted from columnize 0.3.2.
793 # See http://code.google.com/p/pycolumnize/
793 # See http://code.google.com/p/pycolumnize/
794
794
795 # Calculate the number of characters available.
795 width = self._control.viewport().width()
796 width = self._control.viewport().width()
796 char_width = QtGui.QFontMetrics(self.font).width(' ')
797 char_width = QtGui.QFontMetrics(self.font).width(' ')
797 displaywidth = max(5, width / char_width)
798 displaywidth = max(10, (width / char_width) - 1)
798
799
799 # Some degenerate cases.
800 # Some degenerate cases.
800 size = len(items)
801 size = len(items)
801 if size == 0:
802 if size == 0:
802 return '\n'
803 return '\n'
803 elif size == 1:
804 elif size == 1:
804 return '%s\n' % str(items[0])
805 return '%s\n' % str(items[0])
805
806
806 # Try every row count from 1 upwards
807 # Try every row count from 1 upwards
807 array_index = lambda nrows, row, col: nrows*col + row
808 array_index = lambda nrows, row, col: nrows*col + row
808 for nrows in range(1, size):
809 for nrows in range(1, size):
809 ncols = (size + nrows - 1) // nrows
810 ncols = (size + nrows - 1) // nrows
810 colwidths = []
811 colwidths = []
811 totwidth = -len(separator)
812 totwidth = -len(separator)
812 for col in range(ncols):
813 for col in range(ncols):
813 # Get max column width for this column
814 # Get max column width for this column
814 colwidth = 0
815 colwidth = 0
815 for row in range(nrows):
816 for row in range(nrows):
816 i = array_index(nrows, row, col)
817 i = array_index(nrows, row, col)
817 if i >= size: break
818 if i >= size: break
818 x = items[i]
819 x = items[i]
819 colwidth = max(colwidth, len(x))
820 colwidth = max(colwidth, len(x))
820 colwidths.append(colwidth)
821 colwidths.append(colwidth)
821 totwidth += colwidth + len(separator)
822 totwidth += colwidth + len(separator)
822 if totwidth > displaywidth:
823 if totwidth > displaywidth:
823 break
824 break
824 if totwidth <= displaywidth:
825 if totwidth <= displaywidth:
825 break
826 break
826
827
827 # The smallest number of rows computed and the max widths for each
828 # The smallest number of rows computed and the max widths for each
828 # column has been obtained. Now we just have to format each of the rows.
829 # column has been obtained. Now we just have to format each of the rows.
829 string = ''
830 string = ''
830 for row in range(nrows):
831 for row in range(nrows):
831 texts = []
832 texts = []
832 for col in range(ncols):
833 for col in range(ncols):
833 i = row + nrows*col
834 i = row + nrows*col
834 if i >= size:
835 if i >= size:
835 texts.append('')
836 texts.append('')
836 else:
837 else:
837 texts.append(items[i])
838 texts.append(items[i])
838 while texts and not texts[-1]:
839 while texts and not texts[-1]:
839 del texts[-1]
840 del texts[-1]
840 for col in range(len(texts)):
841 for col in range(len(texts)):
841 texts[col] = texts[col].ljust(colwidths[col])
842 texts[col] = texts[col].ljust(colwidths[col])
842 string += '%s\n' % str(separator.join(texts))
843 string += '%s\n' % str(separator.join(texts))
843 return string
844 return string
844
845
845 def _get_block_plain_text(self, block):
846 def _get_block_plain_text(self, block):
846 """ Given a QTextBlock, return its unformatted text.
847 """ Given a QTextBlock, return its unformatted text.
847 """
848 """
848 cursor = QtGui.QTextCursor(block)
849 cursor = QtGui.QTextCursor(block)
849 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
850 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
850 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
851 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
851 QtGui.QTextCursor.KeepAnchor)
852 QtGui.QTextCursor.KeepAnchor)
852 return str(cursor.selection().toPlainText())
853 return str(cursor.selection().toPlainText())
853
854
854 def _get_cursor(self):
855 def _get_cursor(self):
855 """ Convenience method that returns a cursor for the current position.
856 """ Convenience method that returns a cursor for the current position.
856 """
857 """
857 return self._control.textCursor()
858 return self._control.textCursor()
858
859
859 def _get_end_cursor(self):
860 def _get_end_cursor(self):
860 """ Convenience method that returns a cursor for the last character.
861 """ Convenience method that returns a cursor for the last character.
861 """
862 """
862 cursor = self._control.textCursor()
863 cursor = self._control.textCursor()
863 cursor.movePosition(QtGui.QTextCursor.End)
864 cursor.movePosition(QtGui.QTextCursor.End)
864 return cursor
865 return cursor
865
866
866 def _get_input_buffer_cursor_column(self):
867 def _get_input_buffer_cursor_column(self):
867 """ Returns the column of the cursor in the input buffer, excluding the
868 """ Returns the column of the cursor in the input buffer, excluding the
868 contribution by the prompt, or -1 if there is no such column.
869 contribution by the prompt, or -1 if there is no such column.
869 """
870 """
870 prompt = self._get_input_buffer_cursor_prompt()
871 prompt = self._get_input_buffer_cursor_prompt()
871 if prompt is None:
872 if prompt is None:
872 return -1
873 return -1
873 else:
874 else:
874 cursor = self._control.textCursor()
875 cursor = self._control.textCursor()
875 return cursor.columnNumber() - len(prompt)
876 return cursor.columnNumber() - len(prompt)
876
877
877 def _get_input_buffer_cursor_line(self):
878 def _get_input_buffer_cursor_line(self):
878 """ Returns line of the input buffer that contains the cursor, or None
879 """ Returns line of the input buffer that contains the cursor, or None
879 if there is no such line.
880 if there is no such line.
880 """
881 """
881 prompt = self._get_input_buffer_cursor_prompt()
882 prompt = self._get_input_buffer_cursor_prompt()
882 if prompt is None:
883 if prompt is None:
883 return None
884 return None
884 else:
885 else:
885 cursor = self._control.textCursor()
886 cursor = self._control.textCursor()
886 text = self._get_block_plain_text(cursor.block())
887 text = self._get_block_plain_text(cursor.block())
887 return text[len(prompt):]
888 return text[len(prompt):]
888
889
889 def _get_input_buffer_cursor_prompt(self):
890 def _get_input_buffer_cursor_prompt(self):
890 """ Returns the (plain text) prompt for line of the input buffer that
891 """ Returns the (plain text) prompt for line of the input buffer that
891 contains the cursor, or None if there is no such line.
892 contains the cursor, or None if there is no such line.
892 """
893 """
893 if self._executing:
894 if self._executing:
894 return None
895 return None
895 cursor = self._control.textCursor()
896 cursor = self._control.textCursor()
896 if cursor.position() >= self._prompt_pos:
897 if cursor.position() >= self._prompt_pos:
897 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
898 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
898 return self._prompt
899 return self._prompt
899 else:
900 else:
900 return self._continuation_prompt
901 return self._continuation_prompt
901 else:
902 else:
902 return None
903 return None
903
904
904 def _get_prompt_cursor(self):
905 def _get_prompt_cursor(self):
905 """ Convenience method that returns a cursor for the prompt position.
906 """ Convenience method that returns a cursor for the prompt position.
906 """
907 """
907 cursor = self._control.textCursor()
908 cursor = self._control.textCursor()
908 cursor.setPosition(self._prompt_pos)
909 cursor.setPosition(self._prompt_pos)
909 return cursor
910 return cursor
910
911
911 def _get_selection_cursor(self, start, end):
912 def _get_selection_cursor(self, start, end):
912 """ Convenience method that returns a cursor with text selected between
913 """ Convenience method that returns a cursor with text selected between
913 the positions 'start' and 'end'.
914 the positions 'start' and 'end'.
914 """
915 """
915 cursor = self._control.textCursor()
916 cursor = self._control.textCursor()
916 cursor.setPosition(start)
917 cursor.setPosition(start)
917 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
918 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
918 return cursor
919 return cursor
919
920
920 def _get_word_start_cursor(self, position):
921 def _get_word_start_cursor(self, position):
921 """ Find the start of the word to the left the given position. If a
922 """ Find the start of the word to the left the given position. If a
922 sequence of non-word characters precedes the first word, skip over
923 sequence of non-word characters precedes the first word, skip over
923 them. (This emulates the behavior of bash, emacs, etc.)
924 them. (This emulates the behavior of bash, emacs, etc.)
924 """
925 """
925 document = self._control.document()
926 document = self._control.document()
926 position -= 1
927 position -= 1
927 while position >= self._prompt_pos and \
928 while position >= self._prompt_pos and \
928 not document.characterAt(position).isLetterOrNumber():
929 not document.characterAt(position).isLetterOrNumber():
929 position -= 1
930 position -= 1
930 while position >= self._prompt_pos and \
931 while position >= self._prompt_pos and \
931 document.characterAt(position).isLetterOrNumber():
932 document.characterAt(position).isLetterOrNumber():
932 position -= 1
933 position -= 1
933 cursor = self._control.textCursor()
934 cursor = self._control.textCursor()
934 cursor.setPosition(position + 1)
935 cursor.setPosition(position + 1)
935 return cursor
936 return cursor
936
937
937 def _get_word_end_cursor(self, position):
938 def _get_word_end_cursor(self, position):
938 """ Find the end of the word to the right the given position. If a
939 """ Find the end of the word to the right the given position. If a
939 sequence of non-word characters precedes the first word, skip over
940 sequence of non-word characters precedes the first word, skip over
940 them. (This emulates the behavior of bash, emacs, etc.)
941 them. (This emulates the behavior of bash, emacs, etc.)
941 """
942 """
942 document = self._control.document()
943 document = self._control.document()
943 end = self._get_end_cursor().position()
944 end = self._get_end_cursor().position()
944 while position < end and \
945 while position < end and \
945 not document.characterAt(position).isLetterOrNumber():
946 not document.characterAt(position).isLetterOrNumber():
946 position += 1
947 position += 1
947 while position < end and \
948 while position < end and \
948 document.characterAt(position).isLetterOrNumber():
949 document.characterAt(position).isLetterOrNumber():
949 position += 1
950 position += 1
950 cursor = self._control.textCursor()
951 cursor = self._control.textCursor()
951 cursor.setPosition(position)
952 cursor.setPosition(position)
952 return cursor
953 return cursor
953
954
954 def _insert_html(self, cursor, html):
955 def _insert_html(self, cursor, html):
955 """ Inserts HTML using the specified cursor in such a way that future
956 """ Inserts HTML using the specified cursor in such a way that future
956 formatting is unaffected.
957 formatting is unaffected.
957 """
958 """
958 cursor.beginEditBlock()
959 cursor.beginEditBlock()
959 cursor.insertHtml(html)
960 cursor.insertHtml(html)
960
961
961 # After inserting HTML, the text document "remembers" it's in "html
962 # After inserting HTML, the text document "remembers" it's in "html
962 # mode", which means that subsequent calls adding plain text will result
963 # mode", which means that subsequent calls adding plain text will result
963 # in unwanted formatting, lost tab characters, etc. The following code
964 # in unwanted formatting, lost tab characters, etc. The following code
964 # hacks around this behavior, which I consider to be a bug in Qt, by
965 # hacks around this behavior, which I consider to be a bug in Qt, by
965 # (crudely) resetting the document's style state.
966 # (crudely) resetting the document's style state.
966 cursor.movePosition(QtGui.QTextCursor.Left,
967 cursor.movePosition(QtGui.QTextCursor.Left,
967 QtGui.QTextCursor.KeepAnchor)
968 QtGui.QTextCursor.KeepAnchor)
968 if cursor.selection().toPlainText() == ' ':
969 if cursor.selection().toPlainText() == ' ':
969 cursor.removeSelectedText()
970 cursor.removeSelectedText()
970 else:
971 else:
971 cursor.movePosition(QtGui.QTextCursor.Right)
972 cursor.movePosition(QtGui.QTextCursor.Right)
972 cursor.insertText(' ', QtGui.QTextCharFormat())
973 cursor.insertText(' ', QtGui.QTextCharFormat())
973 cursor.endEditBlock()
974 cursor.endEditBlock()
974
975
975 def _insert_html_fetching_plain_text(self, cursor, html):
976 def _insert_html_fetching_plain_text(self, cursor, html):
976 """ Inserts HTML using the specified cursor, then returns its plain text
977 """ Inserts HTML using the specified cursor, then returns its plain text
977 version.
978 version.
978 """
979 """
979 cursor.beginEditBlock()
980 cursor.beginEditBlock()
980 cursor.removeSelectedText()
981 cursor.removeSelectedText()
981
982
982 start = cursor.position()
983 start = cursor.position()
983 self._insert_html(cursor, html)
984 self._insert_html(cursor, html)
984 end = cursor.position()
985 end = cursor.position()
985 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
986 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
986 text = str(cursor.selection().toPlainText())
987 text = str(cursor.selection().toPlainText())
987
988
988 cursor.setPosition(end)
989 cursor.setPosition(end)
989 cursor.endEditBlock()
990 cursor.endEditBlock()
990 return text
991 return text
991
992
992 def _insert_plain_text(self, cursor, text):
993 def _insert_plain_text(self, cursor, text):
993 """ Inserts plain text using the specified cursor, processing ANSI codes
994 """ Inserts plain text using the specified cursor, processing ANSI codes
994 if enabled.
995 if enabled.
995 """
996 """
996 cursor.beginEditBlock()
997 cursor.beginEditBlock()
997 if self.ansi_codes:
998 if self.ansi_codes:
998 for substring in self._ansi_processor.split_string(text):
999 for substring in self._ansi_processor.split_string(text):
999 for action in self._ansi_processor.actions:
1000 for action in self._ansi_processor.actions:
1000 if action.kind == 'erase' and action.area == 'screen':
1001 if action.kind == 'erase' and action.area == 'screen':
1001 cursor.select(QtGui.QTextCursor.Document)
1002 cursor.select(QtGui.QTextCursor.Document)
1002 cursor.removeSelectedText()
1003 cursor.removeSelectedText()
1003 format = self._ansi_processor.get_format()
1004 format = self._ansi_processor.get_format()
1004 cursor.insertText(substring, format)
1005 cursor.insertText(substring, format)
1005 else:
1006 else:
1006 cursor.insertText(text)
1007 cursor.insertText(text)
1007 cursor.endEditBlock()
1008 cursor.endEditBlock()
1008
1009
1009 def _insert_plain_text_into_buffer(self, text):
1010 def _insert_plain_text_into_buffer(self, text):
1010 """ Inserts text into the input buffer at the current cursor position,
1011 """ Inserts text into the input buffer at the current cursor position,
1011 ensuring that continuation prompts are inserted as necessary.
1012 ensuring that continuation prompts are inserted as necessary.
1012 """
1013 """
1013 lines = str(text).splitlines(True)
1014 lines = str(text).splitlines(True)
1014 if lines:
1015 if lines:
1015 self._keep_cursor_in_buffer()
1016 self._keep_cursor_in_buffer()
1016 cursor = self._control.textCursor()
1017 cursor = self._control.textCursor()
1017 cursor.beginEditBlock()
1018 cursor.beginEditBlock()
1018 cursor.insertText(lines[0])
1019 cursor.insertText(lines[0])
1019 for line in lines[1:]:
1020 for line in lines[1:]:
1020 if self._continuation_prompt_html is None:
1021 if self._continuation_prompt_html is None:
1021 cursor.insertText(self._continuation_prompt)
1022 cursor.insertText(self._continuation_prompt)
1022 else:
1023 else:
1023 self._continuation_prompt = \
1024 self._continuation_prompt = \
1024 self._insert_html_fetching_plain_text(
1025 self._insert_html_fetching_plain_text(
1025 cursor, self._continuation_prompt_html)
1026 cursor, self._continuation_prompt_html)
1026 cursor.insertText(line)
1027 cursor.insertText(line)
1027 cursor.endEditBlock()
1028 cursor.endEditBlock()
1028 self._control.setTextCursor(cursor)
1029 self._control.setTextCursor(cursor)
1029
1030
1030 def _in_buffer(self, position=None):
1031 def _in_buffer(self, position=None):
1031 """ Returns whether the current cursor (or, if specified, a position) is
1032 """ Returns whether the current cursor (or, if specified, a position) is
1032 inside the editing region.
1033 inside the editing region.
1033 """
1034 """
1034 cursor = self._control.textCursor()
1035 cursor = self._control.textCursor()
1035 if position is None:
1036 if position is None:
1036 position = cursor.position()
1037 position = cursor.position()
1037 else:
1038 else:
1038 cursor.setPosition(position)
1039 cursor.setPosition(position)
1039 line = cursor.blockNumber()
1040 line = cursor.blockNumber()
1040 prompt_line = self._get_prompt_cursor().blockNumber()
1041 prompt_line = self._get_prompt_cursor().blockNumber()
1041 if line == prompt_line:
1042 if line == prompt_line:
1042 return position >= self._prompt_pos
1043 return position >= self._prompt_pos
1043 elif line > prompt_line:
1044 elif line > prompt_line:
1044 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1045 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1045 prompt_pos = cursor.position() + len(self._continuation_prompt)
1046 prompt_pos = cursor.position() + len(self._continuation_prompt)
1046 return position >= prompt_pos
1047 return position >= prompt_pos
1047 return False
1048 return False
1048
1049
1049 def _keep_cursor_in_buffer(self):
1050 def _keep_cursor_in_buffer(self):
1050 """ Ensures that the cursor is inside the editing region. Returns
1051 """ Ensures that the cursor is inside the editing region. Returns
1051 whether the cursor was moved.
1052 whether the cursor was moved.
1052 """
1053 """
1053 moved = not self._in_buffer()
1054 moved = not self._in_buffer()
1054 if moved:
1055 if moved:
1055 cursor = self._control.textCursor()
1056 cursor = self._control.textCursor()
1056 cursor.movePosition(QtGui.QTextCursor.End)
1057 cursor.movePosition(QtGui.QTextCursor.End)
1057 self._control.setTextCursor(cursor)
1058 self._control.setTextCursor(cursor)
1058 return moved
1059 return moved
1059
1060
1060 def _page(self, text):
1061 def _page(self, text):
1061 """ Displays text using the pager if it exceeds the height of the
1062 """ Displays text using the pager if it exceeds the height of the
1062 visible area.
1063 visible area.
1063 """
1064 """
1064 if self._page_style == 'none':
1065 if self._page_style == 'none':
1065 self._append_plain_text(text)
1066 self._append_plain_text(text)
1066 else:
1067 else:
1067 line_height = QtGui.QFontMetrics(self.font).height()
1068 line_height = QtGui.QFontMetrics(self.font).height()
1068 minlines = self._control.viewport().height() / line_height
1069 minlines = self._control.viewport().height() / line_height
1069 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1070 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1070 if self._page_style == 'custom':
1071 if self._page_style == 'custom':
1071 self.custom_page_requested.emit(text)
1072 self.custom_page_requested.emit(text)
1072 else:
1073 else:
1073 self._page_control.clear()
1074 self._page_control.clear()
1074 cursor = self._page_control.textCursor()
1075 cursor = self._page_control.textCursor()
1075 self._insert_plain_text(cursor, text)
1076 self._insert_plain_text(cursor, text)
1076 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1077 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1077
1078
1078 self._page_control.viewport().resize(self._control.size())
1079 self._page_control.viewport().resize(self._control.size())
1079 if self._splitter:
1080 if self._splitter:
1080 self._page_control.show()
1081 self._page_control.show()
1081 self._page_control.setFocus()
1082 self._page_control.setFocus()
1082 else:
1083 else:
1083 self.layout().setCurrentWidget(self._page_control)
1084 self.layout().setCurrentWidget(self._page_control)
1084 else:
1085 else:
1085 self._append_plain_text(text)
1086 self._append_plain_text(text)
1086
1087
1087 def _prompt_started(self):
1088 def _prompt_started(self):
1088 """ Called immediately after a new prompt is displayed.
1089 """ Called immediately after a new prompt is displayed.
1089 """
1090 """
1090 # Temporarily disable the maximum block count to permit undo/redo and
1091 # Temporarily disable the maximum block count to permit undo/redo and
1091 # to ensure that the prompt position does not change due to truncation.
1092 # to ensure that the prompt position does not change due to truncation.
1092 # Because setting this property clears the undo/redo history, we only
1093 # Because setting this property clears the undo/redo history, we only
1093 # set it if we have to.
1094 # set it if we have to.
1094 if self._control.document().maximumBlockCount() > 0:
1095 if self._control.document().maximumBlockCount() > 0:
1095 self._control.document().setMaximumBlockCount(0)
1096 self._control.document().setMaximumBlockCount(0)
1096 self._control.setUndoRedoEnabled(True)
1097 self._control.setUndoRedoEnabled(True)
1097
1098
1098 self._control.setReadOnly(False)
1099 self._control.setReadOnly(False)
1099 self._control.moveCursor(QtGui.QTextCursor.End)
1100 self._control.moveCursor(QtGui.QTextCursor.End)
1100
1101
1101 self._executing = False
1102 self._executing = False
1102 self._prompt_started_hook()
1103 self._prompt_started_hook()
1103
1104
1104 def _prompt_finished(self):
1105 def _prompt_finished(self):
1105 """ Called immediately after a prompt is finished, i.e. when some input
1106 """ Called immediately after a prompt is finished, i.e. when some input
1106 will be processed and a new prompt displayed.
1107 will be processed and a new prompt displayed.
1107 """
1108 """
1108 self._control.setReadOnly(True)
1109 self._control.setReadOnly(True)
1109 self._prompt_finished_hook()
1110 self._prompt_finished_hook()
1110
1111
1111 def _readline(self, prompt='', callback=None):
1112 def _readline(self, prompt='', callback=None):
1112 """ Reads one line of input from the user.
1113 """ Reads one line of input from the user.
1113
1114
1114 Parameters
1115 Parameters
1115 ----------
1116 ----------
1116 prompt : str, optional
1117 prompt : str, optional
1117 The prompt to print before reading the line.
1118 The prompt to print before reading the line.
1118
1119
1119 callback : callable, optional
1120 callback : callable, optional
1120 A callback to execute with the read line. If not specified, input is
1121 A callback to execute with the read line. If not specified, input is
1121 read *synchronously* and this method does not return until it has
1122 read *synchronously* and this method does not return until it has
1122 been read.
1123 been read.
1123
1124
1124 Returns
1125 Returns
1125 -------
1126 -------
1126 If a callback is specified, returns nothing. Otherwise, returns the
1127 If a callback is specified, returns nothing. Otherwise, returns the
1127 input string with the trailing newline stripped.
1128 input string with the trailing newline stripped.
1128 """
1129 """
1129 if self._reading:
1130 if self._reading:
1130 raise RuntimeError('Cannot read a line. Widget is already reading.')
1131 raise RuntimeError('Cannot read a line. Widget is already reading.')
1131
1132
1132 if not callback and not self.isVisible():
1133 if not callback and not self.isVisible():
1133 # If the user cannot see the widget, this function cannot return.
1134 # If the user cannot see the widget, this function cannot return.
1134 raise RuntimeError('Cannot synchronously read a line if the widget '
1135 raise RuntimeError('Cannot synchronously read a line if the widget '
1135 'is not visible!')
1136 'is not visible!')
1136
1137
1137 self._reading = True
1138 self._reading = True
1138 self._show_prompt(prompt, newline=False)
1139 self._show_prompt(prompt, newline=False)
1139
1140
1140 if callback is None:
1141 if callback is None:
1141 self._reading_callback = None
1142 self._reading_callback = None
1142 while self._reading:
1143 while self._reading:
1143 QtCore.QCoreApplication.processEvents()
1144 QtCore.QCoreApplication.processEvents()
1144 return self.input_buffer.rstrip('\n')
1145 return self.input_buffer.rstrip('\n')
1145
1146
1146 else:
1147 else:
1147 self._reading_callback = lambda: \
1148 self._reading_callback = lambda: \
1148 callback(self.input_buffer.rstrip('\n'))
1149 callback(self.input_buffer.rstrip('\n'))
1149
1150
1150 def _set_continuation_prompt(self, prompt, html=False):
1151 def _set_continuation_prompt(self, prompt, html=False):
1151 """ Sets the continuation prompt.
1152 """ Sets the continuation prompt.
1152
1153
1153 Parameters
1154 Parameters
1154 ----------
1155 ----------
1155 prompt : str
1156 prompt : str
1156 The prompt to show when more input is needed.
1157 The prompt to show when more input is needed.
1157
1158
1158 html : bool, optional (default False)
1159 html : bool, optional (default False)
1159 If set, the prompt will be inserted as formatted HTML. Otherwise,
1160 If set, the prompt will be inserted as formatted HTML. Otherwise,
1160 the prompt will be treated as plain text, though ANSI color codes
1161 the prompt will be treated as plain text, though ANSI color codes
1161 will be handled.
1162 will be handled.
1162 """
1163 """
1163 if html:
1164 if html:
1164 self._continuation_prompt_html = prompt
1165 self._continuation_prompt_html = prompt
1165 else:
1166 else:
1166 self._continuation_prompt = prompt
1167 self._continuation_prompt = prompt
1167 self._continuation_prompt_html = None
1168 self._continuation_prompt_html = None
1168
1169
1169 def _set_cursor(self, cursor):
1170 def _set_cursor(self, cursor):
1170 """ Convenience method to set the current cursor.
1171 """ Convenience method to set the current cursor.
1171 """
1172 """
1172 self._control.setTextCursor(cursor)
1173 self._control.setTextCursor(cursor)
1173
1174
1174 def _set_position(self, position):
1175 def _set_position(self, position):
1175 """ Convenience method to set the position of the cursor.
1176 """ Convenience method to set the position of the cursor.
1176 """
1177 """
1177 cursor = self._control.textCursor()
1178 cursor = self._control.textCursor()
1178 cursor.setPosition(position)
1179 cursor.setPosition(position)
1179 self._control.setTextCursor(cursor)
1180 self._control.setTextCursor(cursor)
1180
1181
1181 def _set_selection(self, start, end):
1182 def _set_selection(self, start, end):
1182 """ Convenience method to set the current selected text.
1183 """ Convenience method to set the current selected text.
1183 """
1184 """
1184 self._control.setTextCursor(self._get_selection_cursor(start, end))
1185 self._control.setTextCursor(self._get_selection_cursor(start, end))
1185
1186
1186 def _show_context_menu(self, pos):
1187 def _show_context_menu(self, pos):
1187 """ Shows a context menu at the given QPoint (in widget coordinates).
1188 """ Shows a context menu at the given QPoint (in widget coordinates).
1188 """
1189 """
1189 menu = QtGui.QMenu()
1190 menu = QtGui.QMenu()
1190
1191
1191 copy_action = menu.addAction('Copy', self.copy)
1192 copy_action = menu.addAction('Copy', self.copy)
1192 copy_action.setEnabled(self._get_cursor().hasSelection())
1193 copy_action.setEnabled(self._get_cursor().hasSelection())
1193 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1194 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1194
1195
1195 paste_action = menu.addAction('Paste', self.paste)
1196 paste_action = menu.addAction('Paste', self.paste)
1196 paste_action.setEnabled(self.can_paste())
1197 paste_action.setEnabled(self.can_paste())
1197 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1198 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1198
1199
1199 menu.addSeparator()
1200 menu.addSeparator()
1200 menu.addAction('Select All', self.select_all)
1201 menu.addAction('Select All', self.select_all)
1201
1202
1202 menu.exec_(self._control.mapToGlobal(pos))
1203 menu.exec_(self._control.mapToGlobal(pos))
1203
1204
1204 def _show_prompt(self, prompt=None, html=False, newline=True):
1205 def _show_prompt(self, prompt=None, html=False, newline=True):
1205 """ Writes a new prompt at the end of the buffer.
1206 """ Writes a new prompt at the end of the buffer.
1206
1207
1207 Parameters
1208 Parameters
1208 ----------
1209 ----------
1209 prompt : str, optional
1210 prompt : str, optional
1210 The prompt to show. If not specified, the previous prompt is used.
1211 The prompt to show. If not specified, the previous prompt is used.
1211
1212
1212 html : bool, optional (default False)
1213 html : bool, optional (default False)
1213 Only relevant when a prompt is specified. If set, the prompt will
1214 Only relevant when a prompt is specified. If set, the prompt will
1214 be inserted as formatted HTML. Otherwise, the prompt will be treated
1215 be inserted as formatted HTML. Otherwise, the prompt will be treated
1215 as plain text, though ANSI color codes will be handled.
1216 as plain text, though ANSI color codes will be handled.
1216
1217
1217 newline : bool, optional (default True)
1218 newline : bool, optional (default True)
1218 If set, a new line will be written before showing the prompt if
1219 If set, a new line will be written before showing the prompt if
1219 there is not already a newline at the end of the buffer.
1220 there is not already a newline at the end of the buffer.
1220 """
1221 """
1221 # Insert a preliminary newline, if necessary.
1222 # Insert a preliminary newline, if necessary.
1222 if newline:
1223 if newline:
1223 cursor = self._get_end_cursor()
1224 cursor = self._get_end_cursor()
1224 if cursor.position() > 0:
1225 if cursor.position() > 0:
1225 cursor.movePosition(QtGui.QTextCursor.Left,
1226 cursor.movePosition(QtGui.QTextCursor.Left,
1226 QtGui.QTextCursor.KeepAnchor)
1227 QtGui.QTextCursor.KeepAnchor)
1227 if str(cursor.selection().toPlainText()) != '\n':
1228 if str(cursor.selection().toPlainText()) != '\n':
1228 self._append_plain_text('\n')
1229 self._append_plain_text('\n')
1229
1230
1230 # Write the prompt.
1231 # Write the prompt.
1231 self._append_plain_text(self._prompt_sep)
1232 self._append_plain_text(self._prompt_sep)
1232 if prompt is None:
1233 if prompt is None:
1233 if self._prompt_html is None:
1234 if self._prompt_html is None:
1234 self._append_plain_text(self._prompt)
1235 self._append_plain_text(self._prompt)
1235 else:
1236 else:
1236 self._append_html(self._prompt_html)
1237 self._append_html(self._prompt_html)
1237 else:
1238 else:
1238 if html:
1239 if html:
1239 self._prompt = self._append_html_fetching_plain_text(prompt)
1240 self._prompt = self._append_html_fetching_plain_text(prompt)
1240 self._prompt_html = prompt
1241 self._prompt_html = prompt
1241 else:
1242 else:
1242 self._append_plain_text(prompt)
1243 self._append_plain_text(prompt)
1243 self._prompt = prompt
1244 self._prompt = prompt
1244 self._prompt_html = None
1245 self._prompt_html = None
1245
1246
1246 self._prompt_pos = self._get_end_cursor().position()
1247 self._prompt_pos = self._get_end_cursor().position()
1247 self._prompt_started()
1248 self._prompt_started()
1248
1249
1249 def _show_continuation_prompt(self):
1250 def _show_continuation_prompt(self):
1250 """ Writes a new continuation prompt at the end of the buffer.
1251 """ Writes a new continuation prompt at the end of the buffer.
1251 """
1252 """
1252 if self._continuation_prompt_html is None:
1253 if self._continuation_prompt_html is None:
1253 self._append_plain_text(self._continuation_prompt)
1254 self._append_plain_text(self._continuation_prompt)
1254 else:
1255 else:
1255 self._continuation_prompt = self._append_html_fetching_plain_text(
1256 self._continuation_prompt = self._append_html_fetching_plain_text(
1256 self._continuation_prompt_html)
1257 self._continuation_prompt_html)
1257
1258
1258
1259
1259 class HistoryConsoleWidget(ConsoleWidget):
1260 class HistoryConsoleWidget(ConsoleWidget):
1260 """ A ConsoleWidget that keeps a history of the commands that have been
1261 """ A ConsoleWidget that keeps a history of the commands that have been
1261 executed.
1262 executed.
1262 """
1263 """
1263
1264
1264 #---------------------------------------------------------------------------
1265 #---------------------------------------------------------------------------
1265 # 'object' interface
1266 # 'object' interface
1266 #---------------------------------------------------------------------------
1267 #---------------------------------------------------------------------------
1267
1268
1268 def __init__(self, *args, **kw):
1269 def __init__(self, *args, **kw):
1269 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1270 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1270 self._history = []
1271 self._history = []
1271 self._history_index = 0
1272 self._history_index = 0
1272
1273
1273 #---------------------------------------------------------------------------
1274 #---------------------------------------------------------------------------
1274 # 'ConsoleWidget' public interface
1275 # 'ConsoleWidget' public interface
1275 #---------------------------------------------------------------------------
1276 #---------------------------------------------------------------------------
1276
1277
1277 def execute(self, source=None, hidden=False, interactive=False):
1278 def execute(self, source=None, hidden=False, interactive=False):
1278 """ Reimplemented to the store history.
1279 """ Reimplemented to the store history.
1279 """
1280 """
1280 if not hidden:
1281 if not hidden:
1281 history = self.input_buffer if source is None else source
1282 history = self.input_buffer if source is None else source
1282
1283
1283 executed = super(HistoryConsoleWidget, self).execute(
1284 executed = super(HistoryConsoleWidget, self).execute(
1284 source, hidden, interactive)
1285 source, hidden, interactive)
1285
1286
1286 if executed and not hidden:
1287 if executed and not hidden:
1287 self._history.append(history.rstrip())
1288 self._history.append(history.rstrip())
1288 self._history_index = len(self._history)
1289 self._history_index = len(self._history)
1289
1290
1290 return executed
1291 return executed
1291
1292
1292 #---------------------------------------------------------------------------
1293 #---------------------------------------------------------------------------
1293 # 'ConsoleWidget' abstract interface
1294 # 'ConsoleWidget' abstract interface
1294 #---------------------------------------------------------------------------
1295 #---------------------------------------------------------------------------
1295
1296
1296 def _up_pressed(self):
1297 def _up_pressed(self):
1297 """ Called when the up key is pressed. Returns whether to continue
1298 """ Called when the up key is pressed. Returns whether to continue
1298 processing the event.
1299 processing the event.
1299 """
1300 """
1300 prompt_cursor = self._get_prompt_cursor()
1301 prompt_cursor = self._get_prompt_cursor()
1301 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1302 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1302 self.history_previous()
1303 self.history_previous()
1303
1304
1304 # Go to the first line of prompt for seemless history scrolling.
1305 # Go to the first line of prompt for seemless history scrolling.
1305 cursor = self._get_prompt_cursor()
1306 cursor = self._get_prompt_cursor()
1306 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1307 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1307 self._set_cursor(cursor)
1308 self._set_cursor(cursor)
1308
1309
1309 return False
1310 return False
1310 return True
1311 return True
1311
1312
1312 def _down_pressed(self):
1313 def _down_pressed(self):
1313 """ Called when the down key is pressed. Returns whether to continue
1314 """ Called when the down key is pressed. Returns whether to continue
1314 processing the event.
1315 processing the event.
1315 """
1316 """
1316 end_cursor = self._get_end_cursor()
1317 end_cursor = self._get_end_cursor()
1317 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1318 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1318 self.history_next()
1319 self.history_next()
1319 return False
1320 return False
1320 return True
1321 return True
1321
1322
1322 #---------------------------------------------------------------------------
1323 #---------------------------------------------------------------------------
1323 # 'HistoryConsoleWidget' public interface
1324 # 'HistoryConsoleWidget' public interface
1324 #---------------------------------------------------------------------------
1325 #---------------------------------------------------------------------------
1325
1326
1326 def history_previous(self):
1327 def history_previous(self):
1327 """ If possible, set the input buffer to the previous item in the
1328 """ If possible, set the input buffer to the previous item in the
1328 history.
1329 history.
1329 """
1330 """
1330 if self._history_index > 0:
1331 if self._history_index > 0:
1331 self._history_index -= 1
1332 self._history_index -= 1
1332 self.input_buffer = self._history[self._history_index]
1333 self.input_buffer = self._history[self._history_index]
1333
1334
1334 def history_next(self):
1335 def history_next(self):
1335 """ Set the input buffer to the next item in the history, or a blank
1336 """ Set the input buffer to the next item in the history, or a blank
1336 line if there is no subsequent item.
1337 line if there is no subsequent item.
1337 """
1338 """
1338 if self._history_index < len(self._history):
1339 if self._history_index < len(self._history):
1339 self._history_index += 1
1340 self._history_index += 1
1340 if self._history_index < len(self._history):
1341 if self._history_index < len(self._history):
1341 self.input_buffer = self._history[self._history_index]
1342 self.input_buffer = self._history[self._history_index]
1342 else:
1343 else:
1343 self.input_buffer = ''
1344 self.input_buffer = ''
1344
1345
1345 #---------------------------------------------------------------------------
1346 #---------------------------------------------------------------------------
1346 # 'HistoryConsoleWidget' protected interface
1347 # 'HistoryConsoleWidget' protected interface
1347 #---------------------------------------------------------------------------
1348 #---------------------------------------------------------------------------
1348
1349
1349 def _set_history(self, history):
1350 def _set_history(self, history):
1350 """ Replace the current history with a sequence of history items.
1351 """ Replace the current history with a sequence of history items.
1351 """
1352 """
1352 self._history = list(history)
1353 self._history = list(history)
1353 self._history_index = len(self._history)
1354 self._history_index = len(self._history)
@@ -1,425 +1,419 b''
1 # Standard library imports
1 # Standard library imports
2 import signal
2 import signal
3 import sys
3 import sys
4
4
5 # System library imports
5 # System library imports
6 from pygments.lexers import PythonLexer
6 from pygments.lexers import PythonLexer
7 from PyQt4 import QtCore, QtGui
7 from PyQt4 import QtCore, QtGui
8 import zmq
8 import zmq
9
9
10 # Local imports
10 # Local imports
11 from IPython.core.inputsplitter import InputSplitter
11 from IPython.core.inputsplitter import InputSplitter
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
13 from call_tip_widget import CallTipWidget
13 from call_tip_widget import CallTipWidget
14 from completion_lexer import CompletionLexer
14 from completion_lexer import CompletionLexer
15 from console_widget import HistoryConsoleWidget
15 from console_widget import HistoryConsoleWidget
16 from pygments_highlighter import PygmentsHighlighter
16 from pygments_highlighter import PygmentsHighlighter
17
17
18
18
19 class FrontendHighlighter(PygmentsHighlighter):
19 class FrontendHighlighter(PygmentsHighlighter):
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 prompts.
21 prompts.
22 """
22 """
23
23
24 def __init__(self, frontend):
24 def __init__(self, frontend):
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 self._current_offset = 0
26 self._current_offset = 0
27 self._frontend = frontend
27 self._frontend = frontend
28 self.highlighting_on = False
28 self.highlighting_on = False
29
29
30 def highlightBlock(self, qstring):
30 def highlightBlock(self, qstring):
31 """ Highlight a block of text. Reimplemented to highlight selectively.
31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 """
32 """
33 if not self.highlighting_on:
33 if not self.highlighting_on:
34 return
34 return
35
35
36 # The input to this function is unicode string that may contain
36 # The input to this function is unicode string that may contain
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 # the string as plain text so we can compare it.
38 # the string as plain text so we can compare it.
39 current_block = self.currentBlock()
39 current_block = self.currentBlock()
40 string = self._frontend._get_block_plain_text(current_block)
40 string = self._frontend._get_block_plain_text(current_block)
41
41
42 # Decide whether to check for the regular or continuation prompt.
42 # Decide whether to check for the regular or continuation prompt.
43 if current_block.contains(self._frontend._prompt_pos):
43 if current_block.contains(self._frontend._prompt_pos):
44 prompt = self._frontend._prompt
44 prompt = self._frontend._prompt
45 else:
45 else:
46 prompt = self._frontend._continuation_prompt
46 prompt = self._frontend._continuation_prompt
47
47
48 # Don't highlight the part of the string that contains the prompt.
48 # Don't highlight the part of the string that contains the prompt.
49 if string.startswith(prompt):
49 if string.startswith(prompt):
50 self._current_offset = len(prompt)
50 self._current_offset = len(prompt)
51 qstring.remove(0, len(prompt))
51 qstring.remove(0, len(prompt))
52 else:
52 else:
53 self._current_offset = 0
53 self._current_offset = 0
54
54
55 PygmentsHighlighter.highlightBlock(self, qstring)
55 PygmentsHighlighter.highlightBlock(self, qstring)
56
56
57 def rehighlightBlock(self, block):
57 def rehighlightBlock(self, block):
58 """ Reimplemented to temporarily enable highlighting if disabled.
58 """ Reimplemented to temporarily enable highlighting if disabled.
59 """
59 """
60 old = self.highlighting_on
60 old = self.highlighting_on
61 self.highlighting_on = True
61 self.highlighting_on = True
62 super(FrontendHighlighter, self).rehighlightBlock(block)
62 super(FrontendHighlighter, self).rehighlightBlock(block)
63 self.highlighting_on = old
63 self.highlighting_on = old
64
64
65 def setFormat(self, start, count, format):
65 def setFormat(self, start, count, format):
66 """ Reimplemented to highlight selectively.
66 """ Reimplemented to highlight selectively.
67 """
67 """
68 start += self._current_offset
68 start += self._current_offset
69 PygmentsHighlighter.setFormat(self, start, count, format)
69 PygmentsHighlighter.setFormat(self, start, count, format)
70
70
71
71
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 """ A Qt frontend for a generic Python kernel.
73 """ A Qt frontend for a generic Python kernel.
74 """
74 """
75
75
76 # An option and corresponding signal for overriding the default kernel
76 # An option and corresponding signal for overriding the default kernel
77 # interrupt behavior.
77 # interrupt behavior.
78 custom_interrupt = False
78 custom_interrupt = False
79 custom_interrupt_requested = QtCore.pyqtSignal()
79 custom_interrupt_requested = QtCore.pyqtSignal()
80
80
81 # An option and corresponding signal for overriding the default kernel
81 # An option and corresponding signal for overriding the default kernel
82 # restart behavior.
82 # restart behavior.
83 custom_restart = False
83 custom_restart = False
84 custom_restart_requested = QtCore.pyqtSignal()
84 custom_restart_requested = QtCore.pyqtSignal()
85
85
86 # Emitted when an 'execute_reply' has been received from the kernel and
86 # Emitted when an 'execute_reply' has been received from the kernel and
87 # processed by the FrontendWidget.
87 # processed by the FrontendWidget.
88 executed = QtCore.pyqtSignal(object)
88 executed = QtCore.pyqtSignal(object)
89
89
90 # Protected class variables.
90 # Protected class variables.
91 _highlighter_class = FrontendHighlighter
91 _highlighter_class = FrontendHighlighter
92 _input_splitter_class = InputSplitter
92 _input_splitter_class = InputSplitter
93
93
94 #---------------------------------------------------------------------------
94 #---------------------------------------------------------------------------
95 # 'object' interface
95 # 'object' interface
96 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
97
97
98 def __init__(self, *args, **kw):
98 def __init__(self, *args, **kw):
99 super(FrontendWidget, self).__init__(*args, **kw)
99 super(FrontendWidget, self).__init__(*args, **kw)
100
100
101 # FrontendWidget protected variables.
101 # FrontendWidget protected variables.
102 self._call_tip_widget = CallTipWidget(self._control)
102 self._call_tip_widget = CallTipWidget(self._control)
103 self._completion_lexer = CompletionLexer(PythonLexer())
103 self._completion_lexer = CompletionLexer(PythonLexer())
104 self._hidden = False
104 self._hidden = False
105 self._highlighter = self._highlighter_class(self)
105 self._highlighter = self._highlighter_class(self)
106 self._input_splitter = self._input_splitter_class(input_mode='block')
106 self._input_splitter = self._input_splitter_class(input_mode='block')
107 self._kernel_manager = None
107 self._kernel_manager = None
108
108
109 # Configure the ConsoleWidget.
109 # Configure the ConsoleWidget.
110 self.tab_width = 4
110 self.tab_width = 4
111 self._set_continuation_prompt('... ')
111 self._set_continuation_prompt('... ')
112
112
113 # Connect signal handlers.
113 # Connect signal handlers.
114 document = self._control.document()
114 document = self._control.document()
115 document.contentsChange.connect(self._document_contents_change)
115 document.contentsChange.connect(self._document_contents_change)
116
116
117 #---------------------------------------------------------------------------
117 #---------------------------------------------------------------------------
118 # 'ConsoleWidget' abstract interface
118 # 'ConsoleWidget' abstract interface
119 #---------------------------------------------------------------------------
119 #---------------------------------------------------------------------------
120
120
121 def _is_complete(self, source, interactive):
121 def _is_complete(self, source, interactive):
122 """ Returns whether 'source' can be completely processed and a new
122 """ Returns whether 'source' can be completely processed and a new
123 prompt created. When triggered by an Enter/Return key press,
123 prompt created. When triggered by an Enter/Return key press,
124 'interactive' is True; otherwise, it is False.
124 'interactive' is True; otherwise, it is False.
125 """
125 """
126 complete = self._input_splitter.push(source.expandtabs(4))
126 complete = self._input_splitter.push(source.expandtabs(4))
127 if interactive:
127 if interactive:
128 complete = not self._input_splitter.push_accepts_more()
128 complete = not self._input_splitter.push_accepts_more()
129 return complete
129 return complete
130
130
131 def _execute(self, source, hidden):
131 def _execute(self, source, hidden):
132 """ Execute 'source'. If 'hidden', do not show any output.
132 """ Execute 'source'. If 'hidden', do not show any output.
133 """
133 """
134 self.kernel_manager.xreq_channel.execute(source, hidden)
134 self.kernel_manager.xreq_channel.execute(source, hidden)
135 self._hidden = hidden
135 self._hidden = hidden
136
136
137 def _prompt_started_hook(self):
137 def _prompt_started_hook(self):
138 """ Called immediately after a new prompt is displayed.
138 """ Called immediately after a new prompt is displayed.
139 """
139 """
140 if not self._reading:
140 if not self._reading:
141 self._highlighter.highlighting_on = True
141 self._highlighter.highlighting_on = True
142
142
143 def _prompt_finished_hook(self):
143 def _prompt_finished_hook(self):
144 """ Called immediately after a prompt is finished, i.e. when some input
144 """ Called immediately after a prompt is finished, i.e. when some input
145 will be processed and a new prompt displayed.
145 will be processed and a new prompt displayed.
146 """
146 """
147 if not self._reading:
147 if not self._reading:
148 self._highlighter.highlighting_on = False
148 self._highlighter.highlighting_on = False
149
149
150 def _tab_pressed(self):
150 def _tab_pressed(self):
151 """ Called when the tab key is pressed. Returns whether to continue
151 """ Called when the tab key is pressed. Returns whether to continue
152 processing the event.
152 processing the event.
153 """
153 """
154 # Perform tab completion if:
154 # Perform tab completion if:
155 # 1) The cursor is in the input buffer.
155 # 1) The cursor is in the input buffer.
156 # 2) There is a non-whitespace character before the cursor.
156 # 2) There is a non-whitespace character before the cursor.
157 text = self._get_input_buffer_cursor_line()
157 text = self._get_input_buffer_cursor_line()
158 if text is None:
158 if text is None:
159 return False
159 return False
160 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
160 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
161 if complete:
161 if complete:
162 self._complete()
162 self._complete()
163 return not complete
163 return not complete
164
164
165 #---------------------------------------------------------------------------
165 #---------------------------------------------------------------------------
166 # 'ConsoleWidget' protected interface
166 # 'ConsoleWidget' protected interface
167 #---------------------------------------------------------------------------
167 #---------------------------------------------------------------------------
168
168
169 def _event_filter_console_keypress(self, event):
169 def _event_filter_console_keypress(self, event):
170 """ Reimplemented to allow execution interruption.
170 """ Reimplemented to allow execution interruption.
171 """
171 """
172 key = event.key()
172 key = event.key()
173 if self._executing and self._control_key_down(event.modifiers()):
173 if self._executing and self._control_key_down(event.modifiers()):
174 if key == QtCore.Qt.Key_C:
174 if key == QtCore.Qt.Key_C:
175 self._kernel_interrupt()
175 self._kernel_interrupt()
176 return True
176 return True
177 elif key == QtCore.Qt.Key_Period:
177 elif key == QtCore.Qt.Key_Period:
178 self._kernel_restart()
178 self._kernel_restart()
179 return True
179 return True
180 return super(FrontendWidget, self)._event_filter_console_keypress(event)
180 return super(FrontendWidget, self)._event_filter_console_keypress(event)
181
181
182 def _show_continuation_prompt(self):
182 def _show_continuation_prompt(self):
183 """ Reimplemented for auto-indentation.
183 """ Reimplemented for auto-indentation.
184 """
184 """
185 super(FrontendWidget, self)._show_continuation_prompt()
185 super(FrontendWidget, self)._show_continuation_prompt()
186 spaces = self._input_splitter.indent_spaces
186 spaces = self._input_splitter.indent_spaces
187 self._append_plain_text('\t' * (spaces / self.tab_width))
187 self._append_plain_text('\t' * (spaces / self.tab_width))
188 self._append_plain_text(' ' * (spaces % self.tab_width))
188 self._append_plain_text(' ' * (spaces % self.tab_width))
189
189
190 #---------------------------------------------------------------------------
190 #---------------------------------------------------------------------------
191 # 'BaseFrontendMixin' abstract interface
191 # 'BaseFrontendMixin' abstract interface
192 #---------------------------------------------------------------------------
192 #---------------------------------------------------------------------------
193
193
194 def _handle_complete_reply(self, rep):
194 def _handle_complete_reply(self, rep):
195 """ Handle replies for tab completion.
195 """ Handle replies for tab completion.
196 """
196 """
197 cursor = self._get_cursor()
197 cursor = self._get_cursor()
198 if rep['parent_header']['msg_id'] == self._complete_id and \
198 if rep['parent_header']['msg_id'] == self._complete_id and \
199 cursor.position() == self._complete_pos:
199 cursor.position() == self._complete_pos:
200 # The completer tells us what text was actually used for the
200 text = '.'.join(self._get_context())
201 # matching, so we must move that many characters left to apply the
202 # completions.
203 text = rep['content']['matched_text']
204 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
201 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
205 self._complete_with_items(cursor, rep['content']['matches'])
202 self._complete_with_items(cursor, rep['content']['matches'])
206
203
207 def _handle_execute_reply(self, msg):
204 def _handle_execute_reply(self, msg):
208 """ Handles replies for code execution.
205 """ Handles replies for code execution.
209 """
206 """
210 if not self._hidden:
207 if not self._hidden:
211 # Make sure that all output from the SUB channel has been processed
208 # Make sure that all output from the SUB channel has been processed
212 # before writing a new prompt.
209 # before writing a new prompt.
213 self.kernel_manager.sub_channel.flush()
210 self.kernel_manager.sub_channel.flush()
214
211
215 content = msg['content']
212 content = msg['content']
216 status = content['status']
213 status = content['status']
217 if status == 'ok':
214 if status == 'ok':
218 self._process_execute_ok(msg)
215 self._process_execute_ok(msg)
219 elif status == 'error':
216 elif status == 'error':
220 self._process_execute_error(msg)
217 self._process_execute_error(msg)
221 elif status == 'abort':
218 elif status == 'abort':
222 self._process_execute_abort(msg)
219 self._process_execute_abort(msg)
223
220
224 self._show_interpreter_prompt_for_reply(msg)
221 self._show_interpreter_prompt_for_reply(msg)
225 self.executed.emit(msg)
222 self.executed.emit(msg)
226
223
227 def _handle_input_request(self, msg):
224 def _handle_input_request(self, msg):
228 """ Handle requests for raw_input.
225 """ Handle requests for raw_input.
229 """
226 """
230 if self._hidden:
227 if self._hidden:
231 raise RuntimeError('Request for raw input during hidden execution.')
228 raise RuntimeError('Request for raw input during hidden execution.')
232
229
233 # Make sure that all output from the SUB channel has been processed
230 # Make sure that all output from the SUB channel has been processed
234 # before entering readline mode.
231 # before entering readline mode.
235 self.kernel_manager.sub_channel.flush()
232 self.kernel_manager.sub_channel.flush()
236
233
237 def callback(line):
234 def callback(line):
238 self.kernel_manager.rep_channel.input(line)
235 self.kernel_manager.rep_channel.input(line)
239 self._readline(msg['content']['prompt'], callback=callback)
236 self._readline(msg['content']['prompt'], callback=callback)
240
237
241 def _handle_object_info_reply(self, rep):
238 def _handle_object_info_reply(self, rep):
242 """ Handle replies for call tips.
239 """ Handle replies for call tips.
243 """
240 """
244 cursor = self._get_cursor()
241 cursor = self._get_cursor()
245 if rep['parent_header']['msg_id'] == self._call_tip_id and \
242 if rep['parent_header']['msg_id'] == self._call_tip_id and \
246 cursor.position() == self._call_tip_pos:
243 cursor.position() == self._call_tip_pos:
247 doc = rep['content']['docstring']
244 doc = rep['content']['docstring']
248 if doc:
245 if doc:
249 self._call_tip_widget.show_docstring(doc)
246 self._call_tip_widget.show_docstring(doc)
250
247
251 def _handle_pyout(self, msg):
248 def _handle_pyout(self, msg):
252 """ Handle display hook output.
249 """ Handle display hook output.
253 """
250 """
254 if not self._hidden and self._is_from_this_session(msg):
251 if not self._hidden and self._is_from_this_session(msg):
255 self._append_plain_text(msg['content']['data'] + '\n')
252 self._append_plain_text(msg['content']['data'] + '\n')
256
253
257 def _handle_stream(self, msg):
254 def _handle_stream(self, msg):
258 """ Handle stdout, stderr, and stdin.
255 """ Handle stdout, stderr, and stdin.
259 """
256 """
260 if not self._hidden and self._is_from_this_session(msg):
257 if not self._hidden and self._is_from_this_session(msg):
261 self._append_plain_text(msg['content']['data'])
258 self._append_plain_text(msg['content']['data'])
262 self._control.moveCursor(QtGui.QTextCursor.End)
259 self._control.moveCursor(QtGui.QTextCursor.End)
263
260
264 def _started_channels(self):
261 def _started_channels(self):
265 """ Called when the KernelManager channels have started listening or
262 """ Called when the KernelManager channels have started listening or
266 when the frontend is assigned an already listening KernelManager.
263 when the frontend is assigned an already listening KernelManager.
267 """
264 """
268 self._control.clear()
265 self._control.clear()
269 self._append_plain_text(self._get_banner())
266 self._append_plain_text(self._get_banner())
270 self._show_interpreter_prompt()
267 self._show_interpreter_prompt()
271
268
272 def _stopped_channels(self):
269 def _stopped_channels(self):
273 """ Called when the KernelManager channels have stopped listening or
270 """ Called when the KernelManager channels have stopped listening or
274 when a listening KernelManager is removed from the frontend.
271 when a listening KernelManager is removed from the frontend.
275 """
272 """
276 self._executing = self._reading = False
273 self._executing = self._reading = False
277 self._highlighter.highlighting_on = False
274 self._highlighter.highlighting_on = False
278
275
279 #---------------------------------------------------------------------------
276 #---------------------------------------------------------------------------
280 # 'FrontendWidget' interface
277 # 'FrontendWidget' interface
281 #---------------------------------------------------------------------------
278 #---------------------------------------------------------------------------
282
279
283 def execute_file(self, path, hidden=False):
280 def execute_file(self, path, hidden=False):
284 """ Attempts to execute file with 'path'. If 'hidden', no output is
281 """ Attempts to execute file with 'path'. If 'hidden', no output is
285 shown.
282 shown.
286 """
283 """
287 self.execute('execfile("%s")' % path, hidden=hidden)
284 self.execute('execfile("%s")' % path, hidden=hidden)
288
285
289 #---------------------------------------------------------------------------
286 #---------------------------------------------------------------------------
290 # 'FrontendWidget' protected interface
287 # 'FrontendWidget' protected interface
291 #---------------------------------------------------------------------------
288 #---------------------------------------------------------------------------
292
289
293 def _call_tip(self):
290 def _call_tip(self):
294 """ Shows a call tip, if appropriate, at the current cursor location.
291 """ Shows a call tip, if appropriate, at the current cursor location.
295 """
292 """
296 # Decide if it makes sense to show a call tip
293 # Decide if it makes sense to show a call tip
297 cursor = self._get_cursor()
294 cursor = self._get_cursor()
298 cursor.movePosition(QtGui.QTextCursor.Left)
295 cursor.movePosition(QtGui.QTextCursor.Left)
299 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
296 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
300 return False
297 return False
301 context = self._get_context(cursor)
298 context = self._get_context(cursor)
302 if not context:
299 if not context:
303 return False
300 return False
304
301
305 # Send the metadata request to the kernel
302 # Send the metadata request to the kernel
306 name = '.'.join(context)
303 name = '.'.join(context)
307 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
304 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
308 self._call_tip_pos = self._get_cursor().position()
305 self._call_tip_pos = self._get_cursor().position()
309 return True
306 return True
310
307
311 def _complete(self):
308 def _complete(self):
312 """ Performs completion at the current cursor location.
309 """ Performs completion at the current cursor location.
313 """
310 """
314 # We let the kernel split the input line, so we *always* send an empty
311 context = self._get_context()
315 # text field. Readline-based frontends do get a real text field which
312 if context:
316 # they can use.
313 # Send the completion request to the kernel
317 text = ''
314 self._complete_id = self.kernel_manager.xreq_channel.complete(
318
315 '.'.join(context), # text
319 # Send the completion request to the kernel
316 self._get_input_buffer_cursor_line(), # line
320 self._complete_id = self.kernel_manager.xreq_channel.complete(
317 self._get_input_buffer_cursor_column(), # cursor_pos
321 text, # text
318 self.input_buffer) # block
322 self._get_input_buffer_cursor_line(), # line
319 self._complete_pos = self._get_cursor().position()
323 self._get_input_buffer_cursor_column(), # cursor_pos
324 self.input_buffer) # block
325 self._complete_pos = self._get_cursor().position()
326
320
327 def _get_banner(self):
321 def _get_banner(self):
328 """ Gets a banner to display at the beginning of a session.
322 """ Gets a banner to display at the beginning of a session.
329 """
323 """
330 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
324 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
331 '"license" for more information.'
325 '"license" for more information.'
332 return banner % (sys.version, sys.platform)
326 return banner % (sys.version, sys.platform)
333
327
334 def _get_context(self, cursor=None):
328 def _get_context(self, cursor=None):
335 """ Gets the context for the specified cursor (or the current cursor
329 """ Gets the context for the specified cursor (or the current cursor
336 if none is specified).
330 if none is specified).
337 """
331 """
338 if cursor is None:
332 if cursor is None:
339 cursor = self._get_cursor()
333 cursor = self._get_cursor()
340 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
334 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
341 QtGui.QTextCursor.KeepAnchor)
335 QtGui.QTextCursor.KeepAnchor)
342 text = str(cursor.selection().toPlainText())
336 text = str(cursor.selection().toPlainText())
343 return self._completion_lexer.get_context(text)
337 return self._completion_lexer.get_context(text)
344
338
345 def _kernel_interrupt(self):
339 def _kernel_interrupt(self):
346 """ Attempts to interrupt the running kernel.
340 """ Attempts to interrupt the running kernel.
347 """
341 """
348 if self.custom_interrupt:
342 if self.custom_interrupt:
349 self.custom_interrupt_requested.emit()
343 self.custom_interrupt_requested.emit()
350 elif self.kernel_manager.has_kernel:
344 elif self.kernel_manager.has_kernel:
351 self.kernel_manager.signal_kernel(signal.SIGINT)
345 self.kernel_manager.signal_kernel(signal.SIGINT)
352 else:
346 else:
353 self._append_plain_text('Kernel process is either remote or '
347 self._append_plain_text('Kernel process is either remote or '
354 'unspecified. Cannot interrupt.\n')
348 'unspecified. Cannot interrupt.\n')
355
349
356 def _kernel_restart(self):
350 def _kernel_restart(self):
357 """ Attempts to restart the running kernel.
351 """ Attempts to restart the running kernel.
358 """
352 """
359 if self.custom_restart:
353 if self.custom_restart:
360 self.custom_restart_requested.emit()
354 self.custom_restart_requested.emit()
361 elif self.kernel_manager.has_kernel:
355 elif self.kernel_manager.has_kernel:
362 try:
356 try:
363 self.kernel_manager.restart_kernel()
357 self.kernel_manager.restart_kernel()
364 except RuntimeError:
358 except RuntimeError:
365 message = 'Kernel started externally. Cannot restart.\n'
359 message = 'Kernel started externally. Cannot restart.\n'
366 self._append_plain_text(message)
360 self._append_plain_text(message)
367 else:
361 else:
368 self._stopped_channels()
362 self._stopped_channels()
369 self._append_plain_text('Kernel restarting...\n')
363 self._append_plain_text('Kernel restarting...\n')
370 self._show_interpreter_prompt()
364 self._show_interpreter_prompt()
371 else:
365 else:
372 self._append_plain_text('Kernel process is either remote or '
366 self._append_plain_text('Kernel process is either remote or '
373 'unspecified. Cannot restart.\n')
367 'unspecified. Cannot restart.\n')
374
368
375 def _process_execute_abort(self, msg):
369 def _process_execute_abort(self, msg):
376 """ Process a reply for an aborted execution request.
370 """ Process a reply for an aborted execution request.
377 """
371 """
378 self._append_plain_text("ERROR: execution aborted\n")
372 self._append_plain_text("ERROR: execution aborted\n")
379
373
380 def _process_execute_error(self, msg):
374 def _process_execute_error(self, msg):
381 """ Process a reply for an execution request that resulted in an error.
375 """ Process a reply for an execution request that resulted in an error.
382 """
376 """
383 content = msg['content']
377 content = msg['content']
384 traceback = ''.join(content['traceback'])
378 traceback = ''.join(content['traceback'])
385 self._append_plain_text(traceback)
379 self._append_plain_text(traceback)
386
380
387 def _process_execute_ok(self, msg):
381 def _process_execute_ok(self, msg):
388 """ Process a reply for a successful execution equest.
382 """ Process a reply for a successful execution equest.
389 """
383 """
390 payload = msg['content']['payload']
384 payload = msg['content']['payload']
391 for item in payload:
385 for item in payload:
392 if not self._process_execute_payload(item):
386 if not self._process_execute_payload(item):
393 warning = 'Received unknown payload of type %s\n'
387 warning = 'Received unknown payload of type %s\n'
394 self._append_plain_text(warning % repr(item['source']))
388 self._append_plain_text(warning % repr(item['source']))
395
389
396 def _process_execute_payload(self, item):
390 def _process_execute_payload(self, item):
397 """ Process a single payload item from the list of payload items in an
391 """ Process a single payload item from the list of payload items in an
398 execution reply. Returns whether the payload was handled.
392 execution reply. Returns whether the payload was handled.
399 """
393 """
400 # The basic FrontendWidget doesn't handle payloads, as they are a
394 # The basic FrontendWidget doesn't handle payloads, as they are a
401 # mechanism for going beyond the standard Python interpreter model.
395 # mechanism for going beyond the standard Python interpreter model.
402 return False
396 return False
403
397
404 def _show_interpreter_prompt(self):
398 def _show_interpreter_prompt(self):
405 """ Shows a prompt for the interpreter.
399 """ Shows a prompt for the interpreter.
406 """
400 """
407 self._show_prompt('>>> ')
401 self._show_prompt('>>> ')
408
402
409 def _show_interpreter_prompt_for_reply(self, msg):
403 def _show_interpreter_prompt_for_reply(self, msg):
410 """ Shows a prompt for the interpreter given an 'execute_reply' message.
404 """ Shows a prompt for the interpreter given an 'execute_reply' message.
411 """
405 """
412 self._show_interpreter_prompt()
406 self._show_interpreter_prompt()
413
407
414 #------ Signal handlers ----------------------------------------------------
408 #------ Signal handlers ----------------------------------------------------
415
409
416 def _document_contents_change(self, position, removed, added):
410 def _document_contents_change(self, position, removed, added):
417 """ Called whenever the document's content changes. Display a call tip
411 """ Called whenever the document's content changes. Display a call tip
418 if appropriate.
412 if appropriate.
419 """
413 """
420 # Calculate where the cursor should be *after* the change:
414 # Calculate where the cursor should be *after* the change:
421 position += added
415 position += added
422
416
423 document = self._control.document()
417 document = self._control.document()
424 if position == self._get_cursor().position():
418 if position == self._get_cursor().position():
425 self._call_tip()
419 self._call_tip()
@@ -1,343 +1,372 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3
3
4 TODO: Add support for retrieving the system default editor. Requires code
4 TODO: Add support for retrieving the system default editor. Requires code
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 Linux (use the xdg system).
6 Linux (use the xdg system).
7 """
7 """
8
8
9 # Standard library imports
9 # Standard library imports
10 from subprocess import Popen
10 from subprocess import Popen
11
11
12 # System library imports
12 # System library imports
13 from PyQt4 import QtCore, QtGui
13 from PyQt4 import QtCore, QtGui
14
14
15 # Local imports
15 # Local imports
16 from IPython.core.inputsplitter import IPythonInputSplitter
16 from IPython.core.inputsplitter import IPythonInputSplitter
17 from IPython.core.usage import default_banner
17 from IPython.core.usage import default_banner
18 from frontend_widget import FrontendWidget
18 from frontend_widget import FrontendWidget
19
19
20
20
21 class IPythonPromptBlock(object):
21 class IPythonPromptBlock(object):
22 """ An internal storage object for IPythonWidget.
22 """ An internal storage object for IPythonWidget.
23 """
23 """
24 def __init__(self, block, length, number):
24 def __init__(self, block, length, number):
25 self.block = block
25 self.block = block
26 self.length = length
26 self.length = length
27 self.number = number
27 self.number = number
28
28
29
29
30 class IPythonWidget(FrontendWidget):
30 class IPythonWidget(FrontendWidget):
31 """ A FrontendWidget for an IPython kernel.
31 """ A FrontendWidget for an IPython kernel.
32 """
32 """
33
33
34 # Signal emitted when an editor is needed for a file and the editor has been
34 # Signal emitted when an editor is needed for a file and the editor has been
35 # specified as 'custom'. See 'set_editor' for more information.
35 # specified as 'custom'. See 'set_editor' for more information.
36 custom_edit_requested = QtCore.pyqtSignal(object, object)
36 custom_edit_requested = QtCore.pyqtSignal(object, object)
37
37
38 # The default stylesheet: black text on a white background.
38 # The default stylesheet: black text on a white background.
39 default_stylesheet = """
39 default_stylesheet = """
40 .error { color: red; }
40 .error { color: red; }
41 .in-prompt { color: navy; }
41 .in-prompt { color: navy; }
42 .in-prompt-number { font-weight: bold; }
42 .in-prompt-number { font-weight: bold; }
43 .out-prompt { color: darkred; }
43 .out-prompt { color: darkred; }
44 .out-prompt-number { font-weight: bold; }
44 .out-prompt-number { font-weight: bold; }
45 """
45 """
46
46
47 # A dark stylesheet: white text on a black background.
47 # A dark stylesheet: white text on a black background.
48 dark_stylesheet = """
48 dark_stylesheet = """
49 QPlainTextEdit, QTextEdit { background-color: black; color: white }
49 QPlainTextEdit, QTextEdit { background-color: black; color: white }
50 QFrame { border: 1px solid grey; }
50 QFrame { border: 1px solid grey; }
51 .error { color: red; }
51 .error { color: red; }
52 .in-prompt { color: lime; }
52 .in-prompt { color: lime; }
53 .in-prompt-number { color: lime; font-weight: bold; }
53 .in-prompt-number { color: lime; font-weight: bold; }
54 .out-prompt { color: red; }
54 .out-prompt { color: red; }
55 .out-prompt-number { color: red; font-weight: bold; }
55 .out-prompt-number { color: red; font-weight: bold; }
56 """
56 """
57
57
58 # Default prompts.
58 # Default prompts.
59 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
59 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
60 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
60 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
61
61
62 # FrontendWidget protected class variables.
62 # FrontendWidget protected class variables.
63 _input_splitter_class = IPythonInputSplitter
63 _input_splitter_class = IPythonInputSplitter
64
64
65 # IPythonWidget protected class variables.
65 # IPythonWidget protected class variables.
66 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
66 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
67 _payload_source_page = 'IPython.zmq.page.page'
67 _payload_source_page = 'IPython.zmq.page.page'
68
68
69 #---------------------------------------------------------------------------
69 #---------------------------------------------------------------------------
70 # 'object' interface
70 # 'object' interface
71 #---------------------------------------------------------------------------
71 #---------------------------------------------------------------------------
72
72
73 def __init__(self, *args, **kw):
73 def __init__(self, *args, **kw):
74 super(IPythonWidget, self).__init__(*args, **kw)
74 super(IPythonWidget, self).__init__(*args, **kw)
75
75
76 # IPythonWidget protected variables.
76 # IPythonWidget protected variables.
77 self._previous_prompt_obj = None
77 self._previous_prompt_obj = None
78
78
79 # Set a default editor and stylesheet.
79 # Set a default editor and stylesheet.
80 self.set_editor('default')
80 self.set_editor('default')
81 self.reset_styling()
81 self.reset_styling()
82
82
83 #---------------------------------------------------------------------------
83 #---------------------------------------------------------------------------
84 # 'BaseFrontendMixin' abstract interface
84 # 'BaseFrontendMixin' abstract interface
85 #---------------------------------------------------------------------------
85 #---------------------------------------------------------------------------
86
86
87 def _handle_complete_reply(self, rep):
88 """ Reimplemented to support IPython's improved completion machinery.
89 """
90 cursor = self._get_cursor()
91 if rep['parent_header']['msg_id'] == self._complete_id and \
92 cursor.position() == self._complete_pos:
93 # The completer tells us what text was actually used for the
94 # matching, so we must move that many characters left to apply the
95 # completions.
96 text = rep['content']['matched_text']
97 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
98 self._complete_with_items(cursor, rep['content']['matches'])
99
87 def _handle_history_reply(self, msg):
100 def _handle_history_reply(self, msg):
88 """ Implemented to handle history replies, which are only supported by
101 """ Implemented to handle history replies, which are only supported by
89 the IPython kernel.
102 the IPython kernel.
90 """
103 """
91 history_dict = msg['content']['history']
104 history_dict = msg['content']['history']
92 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
105 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
93 self._set_history(items)
106 self._set_history(items)
94
107
95 def _handle_prompt_reply(self, msg):
108 def _handle_prompt_reply(self, msg):
96 """ Implemented to handle prompt number replies, which are only
109 """ Implemented to handle prompt number replies, which are only
97 supported by the IPython kernel.
110 supported by the IPython kernel.
98 """
111 """
99 content = msg['content']
112 content = msg['content']
100 self._show_interpreter_prompt(content['prompt_number'],
113 self._show_interpreter_prompt(content['prompt_number'],
101 content['input_sep'])
114 content['input_sep'])
102
115
103 def _handle_pyout(self, msg):
116 def _handle_pyout(self, msg):
104 """ Reimplemented for IPython-style "display hook".
117 """ Reimplemented for IPython-style "display hook".
105 """
118 """
106 if not self._hidden and self._is_from_this_session(msg):
119 if not self._hidden and self._is_from_this_session(msg):
107 content = msg['content']
120 content = msg['content']
108 prompt_number = content['prompt_number']
121 prompt_number = content['prompt_number']
109 self._append_plain_text(content['output_sep'])
122 self._append_plain_text(content['output_sep'])
110 self._append_html(self._make_out_prompt(prompt_number))
123 self._append_html(self._make_out_prompt(prompt_number))
111 self._append_plain_text(content['data'] + '\n' +
124 self._append_plain_text(content['data'] + '\n' +
112 content['output_sep2'])
125 content['output_sep2'])
113
126
114 def _started_channels(self):
127 def _started_channels(self):
115 """ Reimplemented to make a history request.
128 """ Reimplemented to make a history request.
116 """
129 """
117 super(IPythonWidget, self)._started_channels()
130 super(IPythonWidget, self)._started_channels()
118 # FIXME: Disabled until history requests are properly implemented.
131 # FIXME: Disabled until history requests are properly implemented.
119 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
132 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
120
133
121 #---------------------------------------------------------------------------
134 #---------------------------------------------------------------------------
122 # 'FrontendWidget' interface
135 # 'FrontendWidget' interface
123 #---------------------------------------------------------------------------
136 #---------------------------------------------------------------------------
124
137
125 def execute_file(self, path, hidden=False):
138 def execute_file(self, path, hidden=False):
126 """ Reimplemented to use the 'run' magic.
139 """ Reimplemented to use the 'run' magic.
127 """
140 """
128 self.execute('%%run %s' % path, hidden=hidden)
141 self.execute('%%run %s' % path, hidden=hidden)
129
142
130 #---------------------------------------------------------------------------
143 #---------------------------------------------------------------------------
131 # 'FrontendWidget' protected interface
144 # 'FrontendWidget' protected interface
132 #---------------------------------------------------------------------------
145 #---------------------------------------------------------------------------
133
146
147 def _complete(self):
148 """ Reimplemented to support IPython's improved completion machinery.
149 """
150 # We let the kernel split the input line, so we *always* send an empty
151 # text field. Readline-based frontends do get a real text field which
152 # they can use.
153 text = ''
154
155 # Send the completion request to the kernel
156 self._complete_id = self.kernel_manager.xreq_channel.complete(
157 text, # text
158 self._get_input_buffer_cursor_line(), # line
159 self._get_input_buffer_cursor_column(), # cursor_pos
160 self.input_buffer) # block
161 self._complete_pos = self._get_cursor().position()
162
134 def _get_banner(self):
163 def _get_banner(self):
135 """ Reimplemented to return IPython's default banner.
164 """ Reimplemented to return IPython's default banner.
136 """
165 """
137 return default_banner + '\n'
166 return default_banner + '\n'
138
167
139 def _process_execute_error(self, msg):
168 def _process_execute_error(self, msg):
140 """ Reimplemented for IPython-style traceback formatting.
169 """ Reimplemented for IPython-style traceback formatting.
141 """
170 """
142 content = msg['content']
171 content = msg['content']
143 traceback = '\n'.join(content['traceback']) + '\n'
172 traceback = '\n'.join(content['traceback']) + '\n'
144 if False:
173 if False:
145 # FIXME: For now, tracebacks come as plain text, so we can't use
174 # FIXME: For now, tracebacks come as plain text, so we can't use
146 # the html renderer yet. Once we refactor ultratb to produce
175 # the html renderer yet. Once we refactor ultratb to produce
147 # properly styled tracebacks, this branch should be the default
176 # properly styled tracebacks, this branch should be the default
148 traceback = traceback.replace(' ', '&nbsp;')
177 traceback = traceback.replace(' ', '&nbsp;')
149 traceback = traceback.replace('\n', '<br/>')
178 traceback = traceback.replace('\n', '<br/>')
150
179
151 ename = content['ename']
180 ename = content['ename']
152 ename_styled = '<span class="error">%s</span>' % ename
181 ename_styled = '<span class="error">%s</span>' % ename
153 traceback = traceback.replace(ename, ename_styled)
182 traceback = traceback.replace(ename, ename_styled)
154
183
155 self._append_html(traceback)
184 self._append_html(traceback)
156 else:
185 else:
157 # This is the fallback for now, using plain text with ansi escapes
186 # This is the fallback for now, using plain text with ansi escapes
158 self._append_plain_text(traceback)
187 self._append_plain_text(traceback)
159
188
160 def _process_execute_payload(self, item):
189 def _process_execute_payload(self, item):
161 """ Reimplemented to handle %edit and paging payloads.
190 """ Reimplemented to handle %edit and paging payloads.
162 """
191 """
163 if item['source'] == self._payload_source_edit:
192 if item['source'] == self._payload_source_edit:
164 self._edit(item['filename'], item['line_number'])
193 self._edit(item['filename'], item['line_number'])
165 return True
194 return True
166 elif item['source'] == self._payload_source_page:
195 elif item['source'] == self._payload_source_page:
167 self._page(item['data'])
196 self._page(item['data'])
168 return True
197 return True
169 else:
198 else:
170 return False
199 return False
171
200
172 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
201 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
173 """ Reimplemented for IPython-style prompts.
202 """ Reimplemented for IPython-style prompts.
174 """
203 """
175 # If a number was not specified, make a prompt number request.
204 # If a number was not specified, make a prompt number request.
176 if number is None:
205 if number is None:
177 self.kernel_manager.xreq_channel.prompt()
206 self.kernel_manager.xreq_channel.prompt()
178 return
207 return
179
208
180 # Show a new prompt and save information about it so that it can be
209 # Show a new prompt and save information about it so that it can be
181 # updated later if the prompt number turns out to be wrong.
210 # updated later if the prompt number turns out to be wrong.
182 self._prompt_sep = input_sep
211 self._prompt_sep = input_sep
183 self._show_prompt(self._make_in_prompt(number), html=True)
212 self._show_prompt(self._make_in_prompt(number), html=True)
184 block = self._control.document().lastBlock()
213 block = self._control.document().lastBlock()
185 length = len(self._prompt)
214 length = len(self._prompt)
186 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
215 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
187
216
188 # Update continuation prompt to reflect (possibly) new prompt length.
217 # Update continuation prompt to reflect (possibly) new prompt length.
189 self._set_continuation_prompt(
218 self._set_continuation_prompt(
190 self._make_continuation_prompt(self._prompt), html=True)
219 self._make_continuation_prompt(self._prompt), html=True)
191
220
192 def _show_interpreter_prompt_for_reply(self, msg):
221 def _show_interpreter_prompt_for_reply(self, msg):
193 """ Reimplemented for IPython-style prompts.
222 """ Reimplemented for IPython-style prompts.
194 """
223 """
195 # Update the old prompt number if necessary.
224 # Update the old prompt number if necessary.
196 content = msg['content']
225 content = msg['content']
197 previous_prompt_number = content['prompt_number']
226 previous_prompt_number = content['prompt_number']
198 if self._previous_prompt_obj and \
227 if self._previous_prompt_obj and \
199 self._previous_prompt_obj.number != previous_prompt_number:
228 self._previous_prompt_obj.number != previous_prompt_number:
200 block = self._previous_prompt_obj.block
229 block = self._previous_prompt_obj.block
201
230
202 # Make sure the prompt block has not been erased.
231 # Make sure the prompt block has not been erased.
203 if block.isValid() and not block.text().isEmpty():
232 if block.isValid() and not block.text().isEmpty():
204
233
205 # Remove the old prompt and insert a new prompt.
234 # Remove the old prompt and insert a new prompt.
206 cursor = QtGui.QTextCursor(block)
235 cursor = QtGui.QTextCursor(block)
207 cursor.movePosition(QtGui.QTextCursor.Right,
236 cursor.movePosition(QtGui.QTextCursor.Right,
208 QtGui.QTextCursor.KeepAnchor,
237 QtGui.QTextCursor.KeepAnchor,
209 self._previous_prompt_obj.length)
238 self._previous_prompt_obj.length)
210 prompt = self._make_in_prompt(previous_prompt_number)
239 prompt = self._make_in_prompt(previous_prompt_number)
211 self._prompt = self._insert_html_fetching_plain_text(
240 self._prompt = self._insert_html_fetching_plain_text(
212 cursor, prompt)
241 cursor, prompt)
213
242
214 # When the HTML is inserted, Qt blows away the syntax
243 # When the HTML is inserted, Qt blows away the syntax
215 # highlighting for the line, so we need to rehighlight it.
244 # highlighting for the line, so we need to rehighlight it.
216 self._highlighter.rehighlightBlock(cursor.block())
245 self._highlighter.rehighlightBlock(cursor.block())
217
246
218 self._previous_prompt_obj = None
247 self._previous_prompt_obj = None
219
248
220 # Show a new prompt with the kernel's estimated prompt number.
249 # Show a new prompt with the kernel's estimated prompt number.
221 next_prompt = content['next_prompt']
250 next_prompt = content['next_prompt']
222 self._show_interpreter_prompt(next_prompt['prompt_number'],
251 self._show_interpreter_prompt(next_prompt['prompt_number'],
223 next_prompt['input_sep'])
252 next_prompt['input_sep'])
224
253
225 #---------------------------------------------------------------------------
254 #---------------------------------------------------------------------------
226 # 'IPythonWidget' interface
255 # 'IPythonWidget' interface
227 #---------------------------------------------------------------------------
256 #---------------------------------------------------------------------------
228
257
229 def reset_styling(self):
258 def reset_styling(self):
230 """ Restores the default IPythonWidget styling.
259 """ Restores the default IPythonWidget styling.
231 """
260 """
232 self.set_styling(self.default_stylesheet, syntax_style='default')
261 self.set_styling(self.default_stylesheet, syntax_style='default')
233 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
262 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
234
263
235 def set_editor(self, editor, line_editor=None):
264 def set_editor(self, editor, line_editor=None):
236 """ Sets the editor to use with the %edit magic.
265 """ Sets the editor to use with the %edit magic.
237
266
238 Parameters:
267 Parameters:
239 -----------
268 -----------
240 editor : str
269 editor : str
241 A command for invoking a system text editor. If the string contains
270 A command for invoking a system text editor. If the string contains
242 a {filename} format specifier, it will be used. Otherwise, the
271 a {filename} format specifier, it will be used. Otherwise, the
243 filename will be appended to the end the command.
272 filename will be appended to the end the command.
244
273
245 This parameter also takes a special value:
274 This parameter also takes a special value:
246 'custom' : Emit a 'custom_edit_requested(str, int)' signal
275 'custom' : Emit a 'custom_edit_requested(str, int)' signal
247 instead of opening an editor.
276 instead of opening an editor.
248
277
249 line_editor : str, optional
278 line_editor : str, optional
250 The editor command to use when a specific line number is
279 The editor command to use when a specific line number is
251 requested. The string should contain two format specifiers: {line}
280 requested. The string should contain two format specifiers: {line}
252 and {filename}. If this parameter is not specified, the line number
281 and {filename}. If this parameter is not specified, the line number
253 option to the %edit magic will be ignored.
282 option to the %edit magic will be ignored.
254 """
283 """
255 self._editor = editor
284 self._editor = editor
256 self._editor_line = line_editor
285 self._editor_line = line_editor
257
286
258 def set_styling(self, stylesheet, syntax_style=None):
287 def set_styling(self, stylesheet, syntax_style=None):
259 """ Sets the IPythonWidget styling.
288 """ Sets the IPythonWidget styling.
260
289
261 Parameters:
290 Parameters:
262 -----------
291 -----------
263 stylesheet : str
292 stylesheet : str
264 A CSS stylesheet. The stylesheet can contain classes for:
293 A CSS stylesheet. The stylesheet can contain classes for:
265 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
294 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
266 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
295 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
267 3. IPython: .error, .in-prompt, .out-prompt, etc.
296 3. IPython: .error, .in-prompt, .out-prompt, etc.
268
297
269 syntax_style : str or None [default None]
298 syntax_style : str or None [default None]
270 If specified, use the Pygments style with given name. Otherwise,
299 If specified, use the Pygments style with given name. Otherwise,
271 the stylesheet is queried for Pygments style information.
300 the stylesheet is queried for Pygments style information.
272 """
301 """
273 self.setStyleSheet(stylesheet)
302 self.setStyleSheet(stylesheet)
274 self._control.document().setDefaultStyleSheet(stylesheet)
303 self._control.document().setDefaultStyleSheet(stylesheet)
275 if self._page_control:
304 if self._page_control:
276 self._page_control.document().setDefaultStyleSheet(stylesheet)
305 self._page_control.document().setDefaultStyleSheet(stylesheet)
277
306
278 if syntax_style is None:
307 if syntax_style is None:
279 self._highlighter.set_style_sheet(stylesheet)
308 self._highlighter.set_style_sheet(stylesheet)
280 else:
309 else:
281 self._highlighter.set_style(syntax_style)
310 self._highlighter.set_style(syntax_style)
282
311
283 #---------------------------------------------------------------------------
312 #---------------------------------------------------------------------------
284 # 'IPythonWidget' protected interface
313 # 'IPythonWidget' protected interface
285 #---------------------------------------------------------------------------
314 #---------------------------------------------------------------------------
286
315
287 def _edit(self, filename, line=None):
316 def _edit(self, filename, line=None):
288 """ Opens a Python script for editing.
317 """ Opens a Python script for editing.
289
318
290 Parameters:
319 Parameters:
291 -----------
320 -----------
292 filename : str
321 filename : str
293 A path to a local system file.
322 A path to a local system file.
294
323
295 line : int, optional
324 line : int, optional
296 A line of interest in the file.
325 A line of interest in the file.
297 """
326 """
298 if self._editor == 'custom':
327 if self._editor == 'custom':
299 self.custom_edit_requested.emit(filename, line)
328 self.custom_edit_requested.emit(filename, line)
300 elif self._editor == 'default':
329 elif self._editor == 'default':
301 self._append_plain_text('No default editor available.\n')
330 self._append_plain_text('No default editor available.\n')
302 else:
331 else:
303 try:
332 try:
304 filename = '"%s"' % filename
333 filename = '"%s"' % filename
305 if line and self._editor_line:
334 if line and self._editor_line:
306 command = self._editor_line.format(filename=filename,
335 command = self._editor_line.format(filename=filename,
307 line=line)
336 line=line)
308 else:
337 else:
309 try:
338 try:
310 command = self._editor.format()
339 command = self._editor.format()
311 except KeyError:
340 except KeyError:
312 command = self._editor.format(filename=filename)
341 command = self._editor.format(filename=filename)
313 else:
342 else:
314 command += ' ' + filename
343 command += ' ' + filename
315 except KeyError:
344 except KeyError:
316 self._append_plain_text('Invalid editor command.\n')
345 self._append_plain_text('Invalid editor command.\n')
317 else:
346 else:
318 try:
347 try:
319 Popen(command, shell=True)
348 Popen(command, shell=True)
320 except OSError:
349 except OSError:
321 msg = 'Opening editor with command "%s" failed.\n'
350 msg = 'Opening editor with command "%s" failed.\n'
322 self._append_plain_text(msg % command)
351 self._append_plain_text(msg % command)
323
352
324 def _make_in_prompt(self, number):
353 def _make_in_prompt(self, number):
325 """ Given a prompt number, returns an HTML In prompt.
354 """ Given a prompt number, returns an HTML In prompt.
326 """
355 """
327 body = self.in_prompt % number
356 body = self.in_prompt % number
328 return '<span class="in-prompt">%s</span>' % body
357 return '<span class="in-prompt">%s</span>' % body
329
358
330 def _make_continuation_prompt(self, prompt):
359 def _make_continuation_prompt(self, prompt):
331 """ Given a plain text version of an In prompt, returns an HTML
360 """ Given a plain text version of an In prompt, returns an HTML
332 continuation prompt.
361 continuation prompt.
333 """
362 """
334 end_chars = '...: '
363 end_chars = '...: '
335 space_count = len(prompt.lstrip('\n')) - len(end_chars)
364 space_count = len(prompt.lstrip('\n')) - len(end_chars)
336 body = '&nbsp;' * space_count + end_chars
365 body = '&nbsp;' * space_count + end_chars
337 return '<span class="in-prompt">%s</span>' % body
366 return '<span class="in-prompt">%s</span>' % body
338
367
339 def _make_out_prompt(self, number):
368 def _make_out_prompt(self, number):
340 """ Given a prompt number, returns an HTML Out prompt.
369 """ Given a prompt number, returns an HTML Out prompt.
341 """
370 """
342 body = self.out_prompt % number
371 body = self.out_prompt % number
343 return '<span class="out-prompt">%s</span>' % body
372 return '<span class="out-prompt">%s</span>' % body
@@ -1,262 +1,262 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 """A simple interactive kernel that talks to a frontend over 0MQ.
2 """A simple interactive kernel that talks to a frontend over 0MQ.
3
3
4 Things to do:
4 Things to do:
5
5
6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
7 call set_parent on all the PUB objects with the message about to be executed.
7 call set_parent on all the PUB objects with the message about to be executed.
8 * Implement random port and security key logic.
8 * Implement random port and security key logic.
9 * Implement control messages.
9 * Implement control messages.
10 * Implement event loop and poll version.
10 * Implement event loop and poll version.
11 """
11 """
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16
16
17 # Standard library imports.
17 # Standard library imports.
18 import __builtin__
18 import __builtin__
19 from code import CommandCompiler
19 from code import CommandCompiler
20 import sys
20 import sys
21 import time
21 import time
22 import traceback
22 import traceback
23
23
24 # System library imports.
24 # System library imports.
25 import zmq
25 import zmq
26
26
27 # Local imports.
27 # Local imports.
28 from IPython.utils.traitlets import HasTraits, Instance
28 from IPython.utils.traitlets import HasTraits, Instance
29 from completer import KernelCompleter
29 from completer import KernelCompleter
30 from entry_point import base_launch_kernel, make_default_main
30 from entry_point import base_launch_kernel, make_default_main
31 from session import Session, Message
31 from session import Session, Message
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Main kernel class
34 # Main kernel class
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 class Kernel(HasTraits):
37 class Kernel(HasTraits):
38
38
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40 # Kernel interface
40 # Kernel interface
41 #---------------------------------------------------------------------------
41 #---------------------------------------------------------------------------
42
42
43 session = Instance(Session)
43 session = Instance(Session)
44 reply_socket = Instance('zmq.Socket')
44 reply_socket = Instance('zmq.Socket')
45 pub_socket = Instance('zmq.Socket')
45 pub_socket = Instance('zmq.Socket')
46 req_socket = Instance('zmq.Socket')
46 req_socket = Instance('zmq.Socket')
47
47
48 def __init__(self, **kwargs):
48 def __init__(self, **kwargs):
49 super(Kernel, self).__init__(**kwargs)
49 super(Kernel, self).__init__(**kwargs)
50 self.user_ns = {}
50 self.user_ns = {}
51 self.history = []
51 self.history = []
52 self.compiler = CommandCompiler()
52 self.compiler = CommandCompiler()
53 self.completer = KernelCompleter(self.user_ns)
53 self.completer = KernelCompleter(self.user_ns)
54
54
55 # Build dict of handlers for message types
55 # Build dict of handlers for message types
56 msg_types = [ 'execute_request', 'complete_request',
56 msg_types = [ 'execute_request', 'complete_request',
57 'object_info_request' ]
57 'object_info_request' ]
58 self.handlers = {}
58 self.handlers = {}
59 for msg_type in msg_types:
59 for msg_type in msg_types:
60 self.handlers[msg_type] = getattr(self, msg_type)
60 self.handlers[msg_type] = getattr(self, msg_type)
61
61
62 def start(self):
62 def start(self):
63 """ Start the kernel main loop.
63 """ Start the kernel main loop.
64 """
64 """
65 while True:
65 while True:
66 ident = self.reply_socket.recv()
66 ident = self.reply_socket.recv()
67 assert self.reply_socket.rcvmore(), "Missing message part."
67 assert self.reply_socket.rcvmore(), "Missing message part."
68 msg = self.reply_socket.recv_json()
68 msg = self.reply_socket.recv_json()
69 omsg = Message(msg)
69 omsg = Message(msg)
70 print>>sys.__stdout__
70 print>>sys.__stdout__
71 print>>sys.__stdout__, omsg
71 print>>sys.__stdout__, omsg
72 handler = self.handlers.get(omsg.msg_type, None)
72 handler = self.handlers.get(omsg.msg_type, None)
73 if handler is None:
73 if handler is None:
74 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
74 print >> sys.__stderr__, "UNKNOWN MESSAGE TYPE:", omsg
75 else:
75 else:
76 handler(ident, omsg)
76 handler(ident, omsg)
77
77
78 #---------------------------------------------------------------------------
78 #---------------------------------------------------------------------------
79 # Kernel request handlers
79 # Kernel request handlers
80 #---------------------------------------------------------------------------
80 #---------------------------------------------------------------------------
81
81
82 def execute_request(self, ident, parent):
82 def execute_request(self, ident, parent):
83 try:
83 try:
84 code = parent[u'content'][u'code']
84 code = parent[u'content'][u'code']
85 except:
85 except:
86 print>>sys.__stderr__, "Got bad msg: "
86 print>>sys.__stderr__, "Got bad msg: "
87 print>>sys.__stderr__, Message(parent)
87 print>>sys.__stderr__, Message(parent)
88 return
88 return
89 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
89 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
90 self.pub_socket.send_json(pyin_msg)
90 self.pub_socket.send_json(pyin_msg)
91
91
92 try:
92 try:
93 comp_code = self.compiler(code, '<zmq-kernel>')
93 comp_code = self.compiler(code, '<zmq-kernel>')
94
94
95 # Replace raw_input. Note that is not sufficient to replace
95 # Replace raw_input. Note that is not sufficient to replace
96 # raw_input in the user namespace.
96 # raw_input in the user namespace.
97 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
97 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
98 __builtin__.raw_input = raw_input
98 __builtin__.raw_input = raw_input
99
99
100 # Set the parent message of the display hook and out streams.
100 # Set the parent message of the display hook and out streams.
101 sys.displayhook.set_parent(parent)
101 sys.displayhook.set_parent(parent)
102 sys.stdout.set_parent(parent)
102 sys.stdout.set_parent(parent)
103 sys.stderr.set_parent(parent)
103 sys.stderr.set_parent(parent)
104
104
105 exec comp_code in self.user_ns, self.user_ns
105 exec comp_code in self.user_ns, self.user_ns
106 except:
106 except:
107 etype, evalue, tb = sys.exc_info()
107 etype, evalue, tb = sys.exc_info()
108 tb = traceback.format_exception(etype, evalue, tb)
108 tb = traceback.format_exception(etype, evalue, tb)
109 exc_content = {
109 exc_content = {
110 u'status' : u'error',
110 u'status' : u'error',
111 u'traceback' : tb,
111 u'traceback' : tb,
112 u'ename' : unicode(etype.__name__),
112 u'ename' : unicode(etype.__name__),
113 u'evalue' : unicode(evalue)
113 u'evalue' : unicode(evalue)
114 }
114 }
115 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
115 exc_msg = self.session.msg(u'pyerr', exc_content, parent)
116 self.pub_socket.send_json(exc_msg)
116 self.pub_socket.send_json(exc_msg)
117 reply_content = exc_content
117 reply_content = exc_content
118 else:
118 else:
119 reply_content = { 'status' : 'ok', 'payload' : {} }
119 reply_content = { 'status' : 'ok', 'payload' : {} }
120
120
121 # Flush output before sending the reply.
121 # Flush output before sending the reply.
122 sys.stderr.flush()
122 sys.stderr.flush()
123 sys.stdout.flush()
123 sys.stdout.flush()
124
124
125 # Send the reply.
125 # Send the reply.
126 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
126 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
127 print>>sys.__stdout__, Message(reply_msg)
127 print>>sys.__stdout__, Message(reply_msg)
128 self.reply_socket.send(ident, zmq.SNDMORE)
128 self.reply_socket.send(ident, zmq.SNDMORE)
129 self.reply_socket.send_json(reply_msg)
129 self.reply_socket.send_json(reply_msg)
130 if reply_msg['content']['status'] == u'error':
130 if reply_msg['content']['status'] == u'error':
131 self._abort_queue()
131 self._abort_queue()
132
132
133 def complete_request(self, ident, parent):
133 def complete_request(self, ident, parent):
134 matches = {'matches' : self.complete(parent),
134 matches = {'matches' : self._complete(parent),
135 'status' : 'ok'}
135 'status' : 'ok'}
136 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
136 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
137 matches, parent, ident)
137 matches, parent, ident)
138 print >> sys.__stdout__, completion_msg
138 print >> sys.__stdout__, completion_msg
139
139
140 def object_info_request(self, ident, parent):
140 def object_info_request(self, ident, parent):
141 context = parent['content']['oname'].split('.')
141 context = parent['content']['oname'].split('.')
142 object_info = self._object_info(context)
142 object_info = self._object_info(context)
143 msg = self.session.send(self.reply_socket, 'object_info_reply',
143 msg = self.session.send(self.reply_socket, 'object_info_reply',
144 object_info, parent, ident)
144 object_info, parent, ident)
145 print >> sys.__stdout__, msg
145 print >> sys.__stdout__, msg
146
146
147 #---------------------------------------------------------------------------
147 #---------------------------------------------------------------------------
148 # Protected interface
148 # Protected interface
149 #---------------------------------------------------------------------------
149 #---------------------------------------------------------------------------
150
150
151 def _abort_queue(self):
151 def _abort_queue(self):
152 while True:
152 while True:
153 try:
153 try:
154 ident = self.reply_socket.recv(zmq.NOBLOCK)
154 ident = self.reply_socket.recv(zmq.NOBLOCK)
155 except zmq.ZMQError, e:
155 except zmq.ZMQError, e:
156 if e.errno == zmq.EAGAIN:
156 if e.errno == zmq.EAGAIN:
157 break
157 break
158 else:
158 else:
159 assert self.reply_socket.rcvmore(), "Missing message part."
159 assert self.reply_socket.rcvmore(), "Missing message part."
160 msg = self.reply_socket.recv_json()
160 msg = self.reply_socket.recv_json()
161 print>>sys.__stdout__, "Aborting:"
161 print>>sys.__stdout__, "Aborting:"
162 print>>sys.__stdout__, Message(msg)
162 print>>sys.__stdout__, Message(msg)
163 msg_type = msg['msg_type']
163 msg_type = msg['msg_type']
164 reply_type = msg_type.split('_')[0] + '_reply'
164 reply_type = msg_type.split('_')[0] + '_reply'
165 reply_msg = self.session.msg(reply_type, {'status':'aborted'}, msg)
165 reply_msg = self.session.msg(reply_type, {'status':'aborted'}, msg)
166 print>>sys.__stdout__, Message(reply_msg)
166 print>>sys.__stdout__, Message(reply_msg)
167 self.reply_socket.send(ident,zmq.SNDMORE)
167 self.reply_socket.send(ident,zmq.SNDMORE)
168 self.reply_socket.send_json(reply_msg)
168 self.reply_socket.send_json(reply_msg)
169 # We need to wait a bit for requests to come in. This can probably
169 # We need to wait a bit for requests to come in. This can probably
170 # be set shorter for true asynchronous clients.
170 # be set shorter for true asynchronous clients.
171 time.sleep(0.1)
171 time.sleep(0.1)
172
172
173 def _raw_input(self, prompt, ident, parent):
173 def _raw_input(self, prompt, ident, parent):
174 # Flush output before making the request.
174 # Flush output before making the request.
175 sys.stderr.flush()
175 sys.stderr.flush()
176 sys.stdout.flush()
176 sys.stdout.flush()
177
177
178 # Send the input request.
178 # Send the input request.
179 content = dict(prompt=prompt)
179 content = dict(prompt=prompt)
180 msg = self.session.msg(u'input_request', content, parent)
180 msg = self.session.msg(u'input_request', content, parent)
181 self.req_socket.send_json(msg)
181 self.req_socket.send_json(msg)
182
182
183 # Await a response.
183 # Await a response.
184 reply = self.req_socket.recv_json()
184 reply = self.req_socket.recv_json()
185 try:
185 try:
186 value = reply['content']['value']
186 value = reply['content']['value']
187 except:
187 except:
188 print>>sys.__stderr__, "Got bad raw_input reply: "
188 print>>sys.__stderr__, "Got bad raw_input reply: "
189 print>>sys.__stderr__, Message(parent)
189 print>>sys.__stderr__, Message(parent)
190 value = ''
190 value = ''
191 return value
191 return value
192
192
193 def _complete(self, msg):
193 def _complete(self, msg):
194 return self.completer.complete(msg.content.line, msg.content.text)
194 return self.completer.complete(msg.content.line, msg.content.text)
195
195
196 def _object_info(self, context):
196 def _object_info(self, context):
197 symbol, leftover = self._symbol_from_context(context)
197 symbol, leftover = self._symbol_from_context(context)
198 if symbol is not None and not leftover:
198 if symbol is not None and not leftover:
199 doc = getattr(symbol, '__doc__', '')
199 doc = getattr(symbol, '__doc__', '')
200 else:
200 else:
201 doc = ''
201 doc = ''
202 object_info = dict(docstring = doc)
202 object_info = dict(docstring = doc)
203 return object_info
203 return object_info
204
204
205 def _symbol_from_context(self, context):
205 def _symbol_from_context(self, context):
206 if not context:
206 if not context:
207 return None, context
207 return None, context
208
208
209 base_symbol_string = context[0]
209 base_symbol_string = context[0]
210 symbol = self.user_ns.get(base_symbol_string, None)
210 symbol = self.user_ns.get(base_symbol_string, None)
211 if symbol is None:
211 if symbol is None:
212 symbol = __builtin__.__dict__.get(base_symbol_string, None)
212 symbol = __builtin__.__dict__.get(base_symbol_string, None)
213 if symbol is None:
213 if symbol is None:
214 return None, context
214 return None, context
215
215
216 context = context[1:]
216 context = context[1:]
217 for i, name in enumerate(context):
217 for i, name in enumerate(context):
218 new_symbol = getattr(symbol, name, None)
218 new_symbol = getattr(symbol, name, None)
219 if new_symbol is None:
219 if new_symbol is None:
220 return symbol, context[i:]
220 return symbol, context[i:]
221 else:
221 else:
222 symbol = new_symbol
222 symbol = new_symbol
223
223
224 return symbol, []
224 return symbol, []
225
225
226 #-----------------------------------------------------------------------------
226 #-----------------------------------------------------------------------------
227 # Kernel main and launch functions
227 # Kernel main and launch functions
228 #-----------------------------------------------------------------------------
228 #-----------------------------------------------------------------------------
229
229
230 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False):
230 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False):
231 """ Launches a localhost kernel, binding to the specified ports.
231 """ Launches a localhost kernel, binding to the specified ports.
232
232
233 Parameters
233 Parameters
234 ----------
234 ----------
235 xrep_port : int, optional
235 xrep_port : int, optional
236 The port to use for XREP channel.
236 The port to use for XREP channel.
237
237
238 pub_port : int, optional
238 pub_port : int, optional
239 The port to use for the SUB channel.
239 The port to use for the SUB channel.
240
240
241 req_port : int, optional
241 req_port : int, optional
242 The port to use for the REQ (raw input) channel.
242 The port to use for the REQ (raw input) channel.
243
243
244 independent : bool, optional (default False)
244 independent : bool, optional (default False)
245 If set, the kernel process is guaranteed to survive if this process
245 If set, the kernel process is guaranteed to survive if this process
246 dies. If not set, an effort is made to ensure that the kernel is killed
246 dies. If not set, an effort is made to ensure that the kernel is killed
247 when this process dies. Note that in this case it is still good practice
247 when this process dies. Note that in this case it is still good practice
248 to kill kernels manually before exiting.
248 to kill kernels manually before exiting.
249
249
250 Returns
250 Returns
251 -------
251 -------
252 A tuple of form:
252 A tuple of form:
253 (kernel_process, xrep_port, pub_port, req_port)
253 (kernel_process, xrep_port, pub_port, req_port)
254 where kernel_process is a Popen object and the ports are integers.
254 where kernel_process is a Popen object and the ports are integers.
255 """
255 """
256 return base_launch_kernel('from IPython.zmq.pykernel import main; main()',
256 return base_launch_kernel('from IPython.zmq.pykernel import main; main()',
257 xrep_port, pub_port, req_port, independent)
257 xrep_port, pub_port, req_port, independent)
258
258
259 main = make_default_main(Kernel)
259 main = make_default_main(Kernel)
260
260
261 if __name__ == '__main__':
261 if __name__ == '__main__':
262 main()
262 main()
General Comments 0
You need to be logged in to leave comments. Login now