##// END OF EJS Templates
Minor cleanup and bug fix.
epatters -
Show More
@@ -1,1097 +1,1097 b''
1 # Standard library imports
1 # Standard library imports
2 import sys
2 import sys
3 from textwrap import dedent
3 from textwrap import dedent
4
4
5 # System library imports
5 # System library imports
6 from PyQt4 import QtCore, QtGui
6 from PyQt4 import QtCore, QtGui
7
7
8 # Local imports
8 # Local imports
9 from ansi_code_processor import QtAnsiCodeProcessor
9 from ansi_code_processor import QtAnsiCodeProcessor
10 from completion_widget import CompletionWidget
10 from completion_widget import CompletionWidget
11
11
12
12
13 class ConsoleWidget(QtGui.QWidget):
13 class ConsoleWidget(QtGui.QWidget):
14 """ Base class for console-type widgets. This class is mainly concerned with
14 """ Base class for console-type widgets. This class is mainly concerned with
15 dealing with the prompt, keeping the cursor inside the editing line, and
15 dealing with the prompt, keeping the cursor inside the editing line, and
16 handling ANSI escape sequences.
16 handling ANSI escape sequences.
17 """
17 """
18
18
19 # Whether to process ANSI escape codes.
19 # Whether to process ANSI escape codes.
20 ansi_codes = True
20 ansi_codes = True
21
21
22 # The maximum number of lines of text before truncation.
22 # The maximum number of lines of text before truncation.
23 buffer_size = 500
23 buffer_size = 500
24
24
25 # Whether to use a CompletionWidget or plain text output for tab completion.
25 # Whether to use a CompletionWidget or plain text output for tab completion.
26 gui_completion = True
26 gui_completion = True
27
27
28 # Whether to override ShortcutEvents for the keybindings defined by this
28 # Whether to override ShortcutEvents for the keybindings defined by this
29 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
29 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
30 # priority (when it has focus) over, e.g., window-level menu shortcuts.
30 # priority (when it has focus) over, e.g., window-level menu shortcuts.
31 override_shortcuts = False
31 override_shortcuts = False
32
32
33 # Signals that indicate ConsoleWidget state.
33 # Signals that indicate ConsoleWidget state.
34 copy_available = QtCore.pyqtSignal(bool)
34 copy_available = QtCore.pyqtSignal(bool)
35 redo_available = QtCore.pyqtSignal(bool)
35 redo_available = QtCore.pyqtSignal(bool)
36 undo_available = QtCore.pyqtSignal(bool)
36 undo_available = QtCore.pyqtSignal(bool)
37
37
38 # Protected class variables.
38 # Protected class variables.
39 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
39 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
40 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
40 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
41 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
41 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
42 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
42 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
43 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
43 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
44 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
44 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
45 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
45 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
46 _shortcuts = set(_ctrl_down_remap.keys() +
46 _shortcuts = set(_ctrl_down_remap.keys() +
47 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
47 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
48
48
49 #---------------------------------------------------------------------------
49 #---------------------------------------------------------------------------
50 # 'QObject' interface
50 # 'QObject' interface
51 #---------------------------------------------------------------------------
51 #---------------------------------------------------------------------------
52
52
53 def __init__(self, kind='plain', parent=None):
53 def __init__(self, kind='plain', parent=None):
54 """ Create a ConsoleWidget.
54 """ Create a ConsoleWidget.
55
55
56 Parameters
56 Parameters
57 ----------
57 ----------
58 kind : str, optional [default 'plain']
58 kind : str, optional [default 'plain']
59 The type of text widget to use. Valid values are 'plain', which
59 The type of text widget to use. Valid values are 'plain', which
60 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
60 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
61
61
62 parent : QWidget, optional [default None]
62 parent : QWidget, optional [default None]
63 The parent for this widget.
63 The parent for this widget.
64 """
64 """
65 super(ConsoleWidget, self).__init__(parent)
65 super(ConsoleWidget, self).__init__(parent)
66
66
67 # Create the underlying text widget.
67 # Create the underlying text widget.
68 self._control = self._create_control(kind)
68 self._control = self._create_control(kind)
69
69
70 # Initialize protected variables. Some variables contain useful state
70 # Initialize protected variables. Some variables contain useful state
71 # information for subclasses; they should be considered read-only.
71 # information for subclasses; they should be considered read-only.
72 self._ansi_processor = QtAnsiCodeProcessor()
72 self._ansi_processor = QtAnsiCodeProcessor()
73 self._completion_widget = CompletionWidget(self._control)
73 self._completion_widget = CompletionWidget(self._control)
74 self._continuation_prompt = '> '
74 self._continuation_prompt = '> '
75 self._continuation_prompt_html = None
75 self._continuation_prompt_html = None
76 self._executing = False
76 self._executing = False
77 self._prompt = ''
77 self._prompt = ''
78 self._prompt_html = None
78 self._prompt_html = None
79 self._prompt_pos = 0
79 self._prompt_pos = 0
80 self._reading = False
80 self._reading = False
81 self._reading_callback = None
81 self._reading_callback = None
82 self._tab_width = 8
82 self._tab_width = 8
83
83
84 # Set a monospaced font.
84 # Set a monospaced font.
85 self.reset_font()
85 self.reset_font()
86
86
87 def eventFilter(self, obj, event):
87 def eventFilter(self, obj, event):
88 """ Reimplemented to ensure a console-like behavior in the underlying
88 """ Reimplemented to ensure a console-like behavior in the underlying
89 text widget.
89 text widget.
90 """
90 """
91 if obj == self._control:
91 if obj == self._control:
92 etype = event.type()
92 etype = event.type()
93
93
94 # Disable moving text by drag and drop.
94 # Disable moving text by drag and drop.
95 if etype == QtCore.QEvent.DragMove:
95 if etype == QtCore.QEvent.DragMove:
96 return True
96 return True
97
97
98 elif etype == QtCore.QEvent.KeyPress:
98 elif etype == QtCore.QEvent.KeyPress:
99 return self._event_filter_keypress(event)
99 return self._event_filter_keypress(event)
100
100
101 # On Mac OS, it is always unnecessary to override shortcuts, hence
101 # On Mac OS, it is always unnecessary to override shortcuts, hence
102 # the check below. Users should just use the Control key instead of
102 # the check below. Users should just use the Control key instead of
103 # the Command key.
103 # the Command key.
104 elif etype == QtCore.QEvent.ShortcutOverride:
104 elif etype == QtCore.QEvent.ShortcutOverride:
105 if sys.platform != 'darwin' and \
105 if sys.platform != 'darwin' and \
106 self._control_key_down(event.modifiers()) and \
106 self._control_key_down(event.modifiers()) and \
107 event.key() in self._shortcuts:
107 event.key() in self._shortcuts:
108 event.accept()
108 event.accept()
109 return False
109 return False
110
110
111 return super(ConsoleWidget, self).eventFilter(obj, event)
111 return super(ConsoleWidget, self).eventFilter(obj, event)
112
112
113 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
114 # 'ConsoleWidget' public interface
114 # 'ConsoleWidget' public interface
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116
116
117 def can_paste(self):
117 def can_paste(self):
118 """ Returns whether text can be pasted from the clipboard.
118 """ Returns whether text can be pasted from the clipboard.
119 """
119 """
120 # Accept only text that can be ASCII encoded.
120 # Accept only text that can be ASCII encoded.
121 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
121 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
122 text = QtGui.QApplication.clipboard().text()
122 text = QtGui.QApplication.clipboard().text()
123 if not text.isEmpty():
123 if not text.isEmpty():
124 try:
124 try:
125 str(text)
125 str(text)
126 return True
126 return True
127 except UnicodeEncodeError:
127 except UnicodeEncodeError:
128 pass
128 pass
129 return False
129 return False
130
130
131 def clear(self, keep_input=False):
131 def clear(self, keep_input=False):
132 """ Clear the console, then write a new prompt. If 'keep_input' is set,
132 """ Clear the console, then write a new prompt. If 'keep_input' is set,
133 restores the old input buffer when the new prompt is written.
133 restores the old input buffer when the new prompt is written.
134 """
134 """
135 self._control.clear()
135 self._control.clear()
136 if keep_input:
136 if keep_input:
137 input_buffer = self.input_buffer
137 input_buffer = self.input_buffer
138 self._show_prompt()
138 self._show_prompt()
139 if keep_input:
139 if keep_input:
140 self.input_buffer = input_buffer
140 self.input_buffer = input_buffer
141
141
142 def copy(self):
142 def copy(self):
143 """ Copy the current selected text to the clipboard.
143 """ Copy the current selected text to the clipboard.
144 """
144 """
145 self._control.copy()
145 self._control.copy()
146
146
147 def execute(self, source=None, hidden=False, interactive=False):
147 def execute(self, source=None, hidden=False, interactive=False):
148 """ Executes source or the input buffer, possibly prompting for more
148 """ Executes source or the input buffer, possibly prompting for more
149 input.
149 input.
150
150
151 Parameters:
151 Parameters:
152 -----------
152 -----------
153 source : str, optional
153 source : str, optional
154
154
155 The source to execute. If not specified, the input buffer will be
155 The source to execute. If not specified, the input buffer will be
156 used. If specified and 'hidden' is False, the input buffer will be
156 used. If specified and 'hidden' is False, the input buffer will be
157 replaced with the source before execution.
157 replaced with the source before execution.
158
158
159 hidden : bool, optional (default False)
159 hidden : bool, optional (default False)
160
160
161 If set, no output will be shown and the prompt will not be modified.
161 If set, no output will be shown and the prompt will not be modified.
162 In other words, it will be completely invisible to the user that
162 In other words, it will be completely invisible to the user that
163 an execution has occurred.
163 an execution has occurred.
164
164
165 interactive : bool, optional (default False)
165 interactive : bool, optional (default False)
166
166
167 Whether the console is to treat the source as having been manually
167 Whether the console is to treat the source as having been manually
168 entered by the user. The effect of this parameter depends on the
168 entered by the user. The effect of this parameter depends on the
169 subclass implementation.
169 subclass implementation.
170
170
171 Raises:
171 Raises:
172 -------
172 -------
173 RuntimeError
173 RuntimeError
174 If incomplete input is given and 'hidden' is True. In this case,
174 If incomplete input is given and 'hidden' is True. In this case,
175 it not possible to prompt for more input.
175 it is not possible to prompt for more input.
176
176
177 Returns:
177 Returns:
178 --------
178 --------
179 A boolean indicating whether the source was executed.
179 A boolean indicating whether the source was executed.
180 """
180 """
181 if not hidden:
181 if not hidden:
182 if source is not None:
182 if source is not None:
183 self.input_buffer = source
183 self.input_buffer = source
184
184
185 self._append_plain_text('\n')
185 self._append_plain_text('\n')
186 self._executing_input_buffer = self.input_buffer
186 self._executing_input_buffer = self.input_buffer
187 self._executing = True
187 self._executing = True
188 self._prompt_finished()
188 self._prompt_finished()
189
189
190 real_source = self.input_buffer if source is None else source
190 real_source = self.input_buffer if source is None else source
191 complete = self._is_complete(real_source, interactive)
191 complete = self._is_complete(real_source, interactive)
192 if complete:
192 if complete:
193 if not hidden:
193 if not hidden:
194 # The maximum block count is only in effect during execution.
194 # The maximum block count is only in effect during execution.
195 # This ensures that _prompt_pos does not become invalid due to
195 # This ensures that _prompt_pos does not become invalid due to
196 # text truncation.
196 # text truncation.
197 self._control.document().setMaximumBlockCount(self.buffer_size)
197 self._control.document().setMaximumBlockCount(self.buffer_size)
198 self._execute(real_source, hidden)
198 self._execute(real_source, hidden)
199 elif hidden:
199 elif hidden:
200 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
200 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
201 else:
201 else:
202 self._show_continuation_prompt()
202 self._show_continuation_prompt()
203
203
204 return complete
204 return complete
205
205
206 def _get_input_buffer(self):
206 def _get_input_buffer(self):
207 """ The text that the user has entered entered at the current prompt.
207 """ The text that the user has entered entered at the current prompt.
208 """
208 """
209 # If we're executing, the input buffer may not even exist anymore due to
209 # If we're executing, the input buffer may not even exist anymore due to
210 # the limit imposed by 'buffer_size'. Therefore, we store it.
210 # the limit imposed by 'buffer_size'. Therefore, we store it.
211 if self._executing:
211 if self._executing:
212 return self._executing_input_buffer
212 return self._executing_input_buffer
213
213
214 cursor = self._get_end_cursor()
214 cursor = self._get_end_cursor()
215 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
215 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
216 input_buffer = str(cursor.selection().toPlainText())
216 input_buffer = str(cursor.selection().toPlainText())
217
217
218 # Strip out continuation prompts.
218 # Strip out continuation prompts.
219 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
219 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
220
220
221 def _set_input_buffer(self, string):
221 def _set_input_buffer(self, string):
222 """ Replaces the text in the input buffer with 'string'.
222 """ Replaces the text in the input buffer with 'string'.
223 """
223 """
224 # For now, it is an error to modify the input buffer during execution.
224 # For now, it is an error to modify the input buffer during execution.
225 if self._executing:
225 if self._executing:
226 raise RuntimeError("Cannot change input buffer during execution.")
226 raise RuntimeError("Cannot change input buffer during execution.")
227
227
228 # Remove old text.
228 # Remove old text.
229 cursor = self._get_end_cursor()
229 cursor = self._get_end_cursor()
230 cursor.beginEditBlock()
230 cursor.beginEditBlock()
231 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
231 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
232 cursor.removeSelectedText()
232 cursor.removeSelectedText()
233
233
234 # Insert new text with continuation prompts.
234 # Insert new text with continuation prompts.
235 lines = string.splitlines(True)
235 lines = string.splitlines(True)
236 if lines:
236 if lines:
237 self._append_plain_text(lines[0])
237 self._append_plain_text(lines[0])
238 for i in xrange(1, len(lines)):
238 for i in xrange(1, len(lines)):
239 if self._continuation_prompt_html is None:
239 if self._continuation_prompt_html is None:
240 self._append_plain_text(self._continuation_prompt)
240 self._append_plain_text(self._continuation_prompt)
241 else:
241 else:
242 self._append_html(self._continuation_prompt_html)
242 self._append_html(self._continuation_prompt_html)
243 self._append_plain_text(lines[i])
243 self._append_plain_text(lines[i])
244 cursor.endEditBlock()
244 cursor.endEditBlock()
245 self._control.moveCursor(QtGui.QTextCursor.End)
245 self._control.moveCursor(QtGui.QTextCursor.End)
246
246
247 input_buffer = property(_get_input_buffer, _set_input_buffer)
247 input_buffer = property(_get_input_buffer, _set_input_buffer)
248
248
249 def _get_font(self):
249 def _get_font(self):
250 """ The base font being used by the ConsoleWidget.
250 """ The base font being used by the ConsoleWidget.
251 """
251 """
252 return self._control.document().defaultFont()
252 return self._control.document().defaultFont()
253
253
254 def _set_font(self, font):
254 def _set_font(self, font):
255 """ Sets the base font for the ConsoleWidget to the specified QFont.
255 """ Sets the base font for the ConsoleWidget to the specified QFont.
256 """
256 """
257 font_metrics = QtGui.QFontMetrics(font)
257 font_metrics = QtGui.QFontMetrics(font)
258 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
258 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
259
259
260 self._completion_widget.setFont(font)
260 self._completion_widget.setFont(font)
261 self._control.document().setDefaultFont(font)
261 self._control.document().setDefaultFont(font)
262
262
263 font = property(_get_font, _set_font)
263 font = property(_get_font, _set_font)
264
264
265 def paste(self):
265 def paste(self):
266 """ Paste the contents of the clipboard into the input region.
266 """ Paste the contents of the clipboard into the input region.
267 """
267 """
268 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
268 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
269 try:
269 try:
270 text = str(QtGui.QApplication.clipboard().text())
270 text = str(QtGui.QApplication.clipboard().text())
271 except UnicodeEncodeError:
271 except UnicodeEncodeError:
272 pass
272 pass
273 else:
273 else:
274 self._insert_into_buffer(dedent(text))
274 self._insert_into_buffer(dedent(text))
275
275
276 def print_(self, printer):
276 def print_(self, printer):
277 """ Print the contents of the ConsoleWidget to the specified QPrinter.
277 """ Print the contents of the ConsoleWidget to the specified QPrinter.
278 """
278 """
279 self._control.print_(printer)
279 self._control.print_(printer)
280
280
281 def redo(self):
281 def redo(self):
282 """ Redo the last operation. If there is no operation to redo, nothing
282 """ Redo the last operation. If there is no operation to redo, nothing
283 happens.
283 happens.
284 """
284 """
285 self._control.redo()
285 self._control.redo()
286
286
287 def reset_font(self):
287 def reset_font(self):
288 """ Sets the font to the default fixed-width font for this platform.
288 """ Sets the font to the default fixed-width font for this platform.
289 """
289 """
290 if sys.platform == 'win32':
290 if sys.platform == 'win32':
291 name = 'Courier'
291 name = 'Courier'
292 elif sys.platform == 'darwin':
292 elif sys.platform == 'darwin':
293 name = 'Monaco'
293 name = 'Monaco'
294 else:
294 else:
295 name = 'Monospace'
295 name = 'Monospace'
296 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
296 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
297 font.setStyleHint(QtGui.QFont.TypeWriter)
297 font.setStyleHint(QtGui.QFont.TypeWriter)
298 self._set_font(font)
298 self._set_font(font)
299
299
300 def select_all(self):
300 def select_all(self):
301 """ Selects all the text in the buffer.
301 """ Selects all the text in the buffer.
302 """
302 """
303 self._control.selectAll()
303 self._control.selectAll()
304
304
305 def _get_tab_width(self):
305 def _get_tab_width(self):
306 """ The width (in terms of space characters) for tab characters.
306 """ The width (in terms of space characters) for tab characters.
307 """
307 """
308 return self._tab_width
308 return self._tab_width
309
309
310 def _set_tab_width(self, tab_width):
310 def _set_tab_width(self, tab_width):
311 """ Sets the width (in terms of space characters) for tab characters.
311 """ Sets the width (in terms of space characters) for tab characters.
312 """
312 """
313 font_metrics = QtGui.QFontMetrics(self.font)
313 font_metrics = QtGui.QFontMetrics(self.font)
314 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
314 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
315
315
316 self._tab_width = tab_width
316 self._tab_width = tab_width
317
317
318 tab_width = property(_get_tab_width, _set_tab_width)
318 tab_width = property(_get_tab_width, _set_tab_width)
319
319
320 def undo(self):
320 def undo(self):
321 """ Undo the last operation. If there is no operation to undo, nothing
321 """ Undo the last operation. If there is no operation to undo, nothing
322 happens.
322 happens.
323 """
323 """
324 self._control.undo()
324 self._control.undo()
325
325
326 #---------------------------------------------------------------------------
326 #---------------------------------------------------------------------------
327 # 'ConsoleWidget' abstract interface
327 # 'ConsoleWidget' abstract interface
328 #---------------------------------------------------------------------------
328 #---------------------------------------------------------------------------
329
329
330 def _is_complete(self, source, interactive):
330 def _is_complete(self, source, interactive):
331 """ Returns whether 'source' can be executed. When triggered by an
331 """ Returns whether 'source' can be executed. When triggered by an
332 Enter/Return key press, 'interactive' is True; otherwise, it is
332 Enter/Return key press, 'interactive' is True; otherwise, it is
333 False.
333 False.
334 """
334 """
335 raise NotImplementedError
335 raise NotImplementedError
336
336
337 def _execute(self, source, hidden):
337 def _execute(self, source, hidden):
338 """ Execute 'source'. If 'hidden', do not show any output.
338 """ Execute 'source'. If 'hidden', do not show any output.
339 """
339 """
340 raise NotImplementedError
340 raise NotImplementedError
341
341
342 def _execute_interrupt(self):
342 def _execute_interrupt(self):
343 """ Attempts to stop execution. Returns whether this method has an
343 """ Attempts to stop execution. Returns whether this method has an
344 implementation.
344 implementation.
345 """
345 """
346 return False
346 return False
347
347
348 def _prompt_started_hook(self):
348 def _prompt_started_hook(self):
349 """ Called immediately after a new prompt is displayed.
349 """ Called immediately after a new prompt is displayed.
350 """
350 """
351 pass
351 pass
352
352
353 def _prompt_finished_hook(self):
353 def _prompt_finished_hook(self):
354 """ Called immediately after a prompt is finished, i.e. when some input
354 """ Called immediately after a prompt is finished, i.e. when some input
355 will be processed and a new prompt displayed.
355 will be processed and a new prompt displayed.
356 """
356 """
357 pass
357 pass
358
358
359 def _up_pressed(self):
359 def _up_pressed(self):
360 """ Called when the up key is pressed. Returns whether to continue
360 """ Called when the up key is pressed. Returns whether to continue
361 processing the event.
361 processing the event.
362 """
362 """
363 return True
363 return True
364
364
365 def _down_pressed(self):
365 def _down_pressed(self):
366 """ Called when the down key is pressed. Returns whether to continue
366 """ Called when the down key is pressed. Returns whether to continue
367 processing the event.
367 processing the event.
368 """
368 """
369 return True
369 return True
370
370
371 def _tab_pressed(self):
371 def _tab_pressed(self):
372 """ Called when the tab key is pressed. Returns whether to continue
372 """ Called when the tab key is pressed. Returns whether to continue
373 processing the event.
373 processing the event.
374 """
374 """
375 return False
375 return False
376
376
377 #--------------------------------------------------------------------------
377 #--------------------------------------------------------------------------
378 # 'ConsoleWidget' protected interface
378 # 'ConsoleWidget' protected interface
379 #--------------------------------------------------------------------------
379 #--------------------------------------------------------------------------
380
380
381 def _append_html(self, html):
381 def _append_html(self, html):
382 """ Appends html at the end of the console buffer.
382 """ Appends html at the end of the console buffer.
383 """
383 """
384 cursor = self._get_end_cursor()
384 cursor = self._get_end_cursor()
385 self._insert_html(cursor, html)
385 self._insert_html(cursor, html)
386
386
387 def _append_html_fetching_plain_text(self, html):
387 def _append_html_fetching_plain_text(self, html):
388 """ Appends 'html', then returns the plain text version of it.
388 """ Appends 'html', then returns the plain text version of it.
389 """
389 """
390 anchor = self._get_end_cursor().position()
390 anchor = self._get_end_cursor().position()
391 self._append_html(html)
391 self._append_html(html)
392 cursor = self._get_end_cursor()
392 cursor = self._get_end_cursor()
393 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
393 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
394 return str(cursor.selection().toPlainText())
394 return str(cursor.selection().toPlainText())
395
395
396 def _append_plain_text(self, text):
396 def _append_plain_text(self, text):
397 """ Appends plain text at the end of the console buffer, processing
397 """ Appends plain text at the end of the console buffer, processing
398 ANSI codes if enabled.
398 ANSI codes if enabled.
399 """
399 """
400 cursor = self._get_end_cursor()
400 cursor = self._get_end_cursor()
401 cursor.beginEditBlock()
401 cursor.beginEditBlock()
402 if self.ansi_codes:
402 if self.ansi_codes:
403 for substring in self._ansi_processor.split_string(text):
403 for substring in self._ansi_processor.split_string(text):
404 format = self._ansi_processor.get_format()
404 format = self._ansi_processor.get_format()
405 cursor.insertText(substring, format)
405 cursor.insertText(substring, format)
406 else:
406 else:
407 cursor.insertText(text)
407 cursor.insertText(text)
408 cursor.endEditBlock()
408 cursor.endEditBlock()
409
409
410 def _append_plain_text_keeping_prompt(self, text):
410 def _append_plain_text_keeping_prompt(self, text):
411 """ Writes 'text' after the current prompt, then restores the old prompt
411 """ Writes 'text' after the current prompt, then restores the old prompt
412 with its old input buffer.
412 with its old input buffer.
413 """
413 """
414 input_buffer = self.input_buffer
414 input_buffer = self.input_buffer
415 self._append_plain_text('\n')
415 self._append_plain_text('\n')
416 self._prompt_finished()
416 self._prompt_finished()
417
417
418 self._append_plain_text(text)
418 self._append_plain_text(text)
419 self._show_prompt()
419 self._show_prompt()
420 self.input_buffer = input_buffer
420 self.input_buffer = input_buffer
421
421
422 def _complete_with_items(self, cursor, items):
422 def _complete_with_items(self, cursor, items):
423 """ Performs completion with 'items' at the specified cursor location.
423 """ Performs completion with 'items' at the specified cursor location.
424 """
424 """
425 if len(items) == 1:
425 if len(items) == 1:
426 cursor.setPosition(self._control.textCursor().position(),
426 cursor.setPosition(self._control.textCursor().position(),
427 QtGui.QTextCursor.KeepAnchor)
427 QtGui.QTextCursor.KeepAnchor)
428 cursor.insertText(items[0])
428 cursor.insertText(items[0])
429 elif len(items) > 1:
429 elif len(items) > 1:
430 if self.gui_completion:
430 if self.gui_completion:
431 self._completion_widget.show_items(cursor, items)
431 self._completion_widget.show_items(cursor, items)
432 else:
432 else:
433 text = self._format_as_columns(items)
433 text = self._format_as_columns(items)
434 self._append_plain_text_keeping_prompt(text)
434 self._append_plain_text_keeping_prompt(text)
435
435
436 def _control_key_down(self, modifiers):
436 def _control_key_down(self, modifiers):
437 """ Given a KeyboardModifiers flags object, return whether the Control
437 """ Given a KeyboardModifiers flags object, return whether the Control
438 key is down (on Mac OS, treat the Command key as a synonym for
438 key is down (on Mac OS, treat the Command key as a synonym for
439 Control).
439 Control).
440 """
440 """
441 down = bool(modifiers & QtCore.Qt.ControlModifier)
441 down = bool(modifiers & QtCore.Qt.ControlModifier)
442
442
443 # Note: on Mac OS, ControlModifier corresponds to the Command key while
443 # Note: on Mac OS, ControlModifier corresponds to the Command key while
444 # MetaModifier corresponds to the Control key.
444 # MetaModifier corresponds to the Control key.
445 if sys.platform == 'darwin':
445 if sys.platform == 'darwin':
446 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
446 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
447
447
448 return down
448 return down
449
449
450 def _create_control(self, kind):
450 def _create_control(self, kind):
451 """ Creates and sets the underlying text widget.
451 """ Creates and sets the underlying text widget.
452 """
452 """
453 layout = QtGui.QVBoxLayout(self)
453 layout = QtGui.QVBoxLayout(self)
454 layout.setMargin(0)
454 layout.setMargin(0)
455 if kind == 'plain':
455 if kind == 'plain':
456 control = QtGui.QPlainTextEdit()
456 control = QtGui.QPlainTextEdit()
457 elif kind == 'rich':
457 elif kind == 'rich':
458 control = QtGui.QTextEdit()
458 control = QtGui.QTextEdit()
459 control.setAcceptRichText(False)
459 control.setAcceptRichText(False)
460 else:
460 else:
461 raise ValueError("Kind %s unknown." % repr(kind))
461 raise ValueError("Kind %s unknown." % repr(kind))
462 layout.addWidget(control)
462 layout.addWidget(control)
463
463
464 control.installEventFilter(self)
464 control.installEventFilter(self)
465 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
465 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
466 control.customContextMenuRequested.connect(self._show_context_menu)
466 control.customContextMenuRequested.connect(self._show_context_menu)
467 control.copyAvailable.connect(self.copy_available)
467 control.copyAvailable.connect(self.copy_available)
468 control.redoAvailable.connect(self.redo_available)
468 control.redoAvailable.connect(self.redo_available)
469 control.undoAvailable.connect(self.undo_available)
469 control.undoAvailable.connect(self.undo_available)
470
470
471 return control
471 return control
472
472
473 def _event_filter_keypress(self, event):
473 def _event_filter_keypress(self, event):
474 """ Filter key events for the underlying text widget to create a
474 """ Filter key events for the underlying text widget to create a
475 console-like interface.
475 console-like interface.
476 """
476 """
477 key = event.key()
477 key = event.key()
478 ctrl_down = self._control_key_down(event.modifiers())
478 ctrl_down = self._control_key_down(event.modifiers())
479
479
480 # If the key is remapped, return immediately.
480 # If the key is remapped, return immediately.
481 if ctrl_down and key in self._ctrl_down_remap:
481 if ctrl_down and key in self._ctrl_down_remap:
482 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
482 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
483 self._ctrl_down_remap[key],
483 self._ctrl_down_remap[key],
484 QtCore.Qt.NoModifier)
484 QtCore.Qt.NoModifier)
485 QtGui.qApp.sendEvent(self._control, new_event)
485 QtGui.qApp.sendEvent(self._control, new_event)
486 return True
486 return True
487
487
488 # Otherwise, proceed normally and do not return early.
488 # Otherwise, proceed normally and do not return early.
489 intercepted = False
489 intercepted = False
490 cursor = self._control.textCursor()
490 cursor = self._control.textCursor()
491 position = cursor.position()
491 position = cursor.position()
492 alt_down = event.modifiers() & QtCore.Qt.AltModifier
492 alt_down = event.modifiers() & QtCore.Qt.AltModifier
493 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
493 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
494
494
495 if event.matches(QtGui.QKeySequence.Paste):
495 if event.matches(QtGui.QKeySequence.Paste):
496 # Call our paste instead of the underlying text widget's.
496 # Call our paste instead of the underlying text widget's.
497 self.paste()
497 self.paste()
498 intercepted = True
498 intercepted = True
499
499
500 elif ctrl_down:
500 elif ctrl_down:
501 if key == QtCore.Qt.Key_C:
501 if key == QtCore.Qt.Key_C:
502 intercepted = self._executing and self._execute_interrupt()
502 intercepted = self._executing and self._execute_interrupt()
503
503
504 elif key == QtCore.Qt.Key_K:
504 elif key == QtCore.Qt.Key_K:
505 if self._in_buffer(position):
505 if self._in_buffer(position):
506 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
506 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
507 QtGui.QTextCursor.KeepAnchor)
507 QtGui.QTextCursor.KeepAnchor)
508 cursor.removeSelectedText()
508 cursor.removeSelectedText()
509 intercepted = True
509 intercepted = True
510
510
511 elif key == QtCore.Qt.Key_X:
511 elif key == QtCore.Qt.Key_X:
512 intercepted = True
512 intercepted = True
513
513
514 elif key == QtCore.Qt.Key_Y:
514 elif key == QtCore.Qt.Key_Y:
515 self.paste()
515 self.paste()
516 intercepted = True
516 intercepted = True
517
517
518 elif alt_down:
518 elif alt_down:
519 if key == QtCore.Qt.Key_B:
519 if key == QtCore.Qt.Key_B:
520 self._set_cursor(self._get_word_start_cursor(position))
520 self._set_cursor(self._get_word_start_cursor(position))
521 intercepted = True
521 intercepted = True
522
522
523 elif key == QtCore.Qt.Key_F:
523 elif key == QtCore.Qt.Key_F:
524 self._set_cursor(self._get_word_end_cursor(position))
524 self._set_cursor(self._get_word_end_cursor(position))
525 intercepted = True
525 intercepted = True
526
526
527 elif key == QtCore.Qt.Key_Backspace:
527 elif key == QtCore.Qt.Key_Backspace:
528 cursor = self._get_word_start_cursor(position)
528 cursor = self._get_word_start_cursor(position)
529 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
529 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
530 cursor.removeSelectedText()
530 cursor.removeSelectedText()
531 intercepted = True
531 intercepted = True
532
532
533 elif key == QtCore.Qt.Key_D:
533 elif key == QtCore.Qt.Key_D:
534 cursor = self._get_word_end_cursor(position)
534 cursor = self._get_word_end_cursor(position)
535 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
535 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
536 cursor.removeSelectedText()
536 cursor.removeSelectedText()
537 intercepted = True
537 intercepted = True
538
538
539 else:
539 else:
540 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
540 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
541 if self._reading:
541 if self._reading:
542 self._append_plain_text('\n')
542 self._append_plain_text('\n')
543 self._reading = False
543 self._reading = False
544 if self._reading_callback:
544 if self._reading_callback:
545 self._reading_callback()
545 self._reading_callback()
546 elif not self._executing:
546 elif not self._executing:
547 self.execute(interactive=True)
547 self.execute(interactive=True)
548 intercepted = True
548 intercepted = True
549
549
550 elif key == QtCore.Qt.Key_Up:
550 elif key == QtCore.Qt.Key_Up:
551 if self._reading or not self._up_pressed():
551 if self._reading or not self._up_pressed():
552 intercepted = True
552 intercepted = True
553 else:
553 else:
554 prompt_line = self._get_prompt_cursor().blockNumber()
554 prompt_line = self._get_prompt_cursor().blockNumber()
555 intercepted = cursor.blockNumber() <= prompt_line
555 intercepted = cursor.blockNumber() <= prompt_line
556
556
557 elif key == QtCore.Qt.Key_Down:
557 elif key == QtCore.Qt.Key_Down:
558 if self._reading or not self._down_pressed():
558 if self._reading or not self._down_pressed():
559 intercepted = True
559 intercepted = True
560 else:
560 else:
561 end_line = self._get_end_cursor().blockNumber()
561 end_line = self._get_end_cursor().blockNumber()
562 intercepted = cursor.blockNumber() == end_line
562 intercepted = cursor.blockNumber() == end_line
563
563
564 elif key == QtCore.Qt.Key_Tab:
564 elif key == QtCore.Qt.Key_Tab:
565 if self._reading:
565 if self._reading:
566 intercepted = False
566 intercepted = False
567 else:
567 else:
568 intercepted = not self._tab_pressed()
568 intercepted = not self._tab_pressed()
569
569
570 elif key == QtCore.Qt.Key_Left:
570 elif key == QtCore.Qt.Key_Left:
571 intercepted = not self._in_buffer(position - 1)
571 intercepted = not self._in_buffer(position - 1)
572
572
573 elif key == QtCore.Qt.Key_Home:
573 elif key == QtCore.Qt.Key_Home:
574 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
574 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
575 start_line = cursor.blockNumber()
575 start_line = cursor.blockNumber()
576 if start_line == self._get_prompt_cursor().blockNumber():
576 if start_line == self._get_prompt_cursor().blockNumber():
577 start_pos = self._prompt_pos
577 start_pos = self._prompt_pos
578 else:
578 else:
579 start_pos = cursor.position()
579 start_pos = cursor.position()
580 start_pos += len(self._continuation_prompt)
580 start_pos += len(self._continuation_prompt)
581 if shift_down and self._in_buffer(position):
581 if shift_down and self._in_buffer(position):
582 self._set_selection(position, start_pos)
582 self._set_selection(position, start_pos)
583 else:
583 else:
584 self._set_position(start_pos)
584 self._set_position(start_pos)
585 intercepted = True
585 intercepted = True
586
586
587 elif key == QtCore.Qt.Key_Backspace:
587 elif key == QtCore.Qt.Key_Backspace:
588
588
589 # Line deletion (remove continuation prompt)
589 # Line deletion (remove continuation prompt)
590 len_prompt = len(self._continuation_prompt)
590 len_prompt = len(self._continuation_prompt)
591 if not self._reading and \
591 if not self._reading and \
592 cursor.columnNumber() == len_prompt and \
592 cursor.columnNumber() == len_prompt and \
593 position != self._prompt_pos:
593 position != self._prompt_pos:
594 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
594 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
595 QtGui.QTextCursor.KeepAnchor)
595 QtGui.QTextCursor.KeepAnchor)
596 cursor.removeSelectedText()
596 cursor.removeSelectedText()
597
597
598 # Regular backwards deletion
598 # Regular backwards deletion
599 else:
599 else:
600 anchor = cursor.anchor()
600 anchor = cursor.anchor()
601 if anchor == position:
601 if anchor == position:
602 intercepted = not self._in_buffer(position - 1)
602 intercepted = not self._in_buffer(position - 1)
603 else:
603 else:
604 intercepted = not self._in_buffer(min(anchor, position))
604 intercepted = not self._in_buffer(min(anchor, position))
605
605
606 elif key == QtCore.Qt.Key_Delete:
606 elif key == QtCore.Qt.Key_Delete:
607 anchor = cursor.anchor()
607 anchor = cursor.anchor()
608 intercepted = not self._in_buffer(min(anchor, position))
608 intercepted = not self._in_buffer(min(anchor, position))
609
609
610 # Don't move the cursor if control is down to allow copy-paste using
610 # Don't move the cursor if control is down to allow copy-paste using
611 # the keyboard in any part of the buffer.
611 # the keyboard in any part of the buffer.
612 if not ctrl_down:
612 if not ctrl_down:
613 self._keep_cursor_in_buffer()
613 self._keep_cursor_in_buffer()
614
614
615 return intercepted
615 return intercepted
616
616
617 def _format_as_columns(self, items, separator=' '):
617 def _format_as_columns(self, items, separator=' '):
618 """ Transform a list of strings into a single string with columns.
618 """ Transform a list of strings into a single string with columns.
619
619
620 Parameters
620 Parameters
621 ----------
621 ----------
622 items : sequence of strings
622 items : sequence of strings
623 The strings to process.
623 The strings to process.
624
624
625 separator : str, optional [default is two spaces]
625 separator : str, optional [default is two spaces]
626 The string that separates columns.
626 The string that separates columns.
627
627
628 Returns
628 Returns
629 -------
629 -------
630 The formatted string.
630 The formatted string.
631 """
631 """
632 # Note: this code is adapted from columnize 0.3.2.
632 # Note: this code is adapted from columnize 0.3.2.
633 # See http://code.google.com/p/pycolumnize/
633 # See http://code.google.com/p/pycolumnize/
634
634
635 font_metrics = QtGui.QFontMetrics(self.font)
635 font_metrics = QtGui.QFontMetrics(self.font)
636 displaywidth = max(5, (self.width() / font_metrics.width(' ')) - 1)
636 displaywidth = max(5, (self.width() / font_metrics.width(' ')) - 1)
637
637
638 # Some degenerate cases.
638 # Some degenerate cases.
639 size = len(items)
639 size = len(items)
640 if size == 0:
640 if size == 0:
641 return '\n'
641 return '\n'
642 elif size == 1:
642 elif size == 1:
643 return '%s\n' % str(items[0])
643 return '%s\n' % str(items[0])
644
644
645 # Try every row count from 1 upwards
645 # Try every row count from 1 upwards
646 array_index = lambda nrows, row, col: nrows*col + row
646 array_index = lambda nrows, row, col: nrows*col + row
647 for nrows in range(1, size):
647 for nrows in range(1, size):
648 ncols = (size + nrows - 1) // nrows
648 ncols = (size + nrows - 1) // nrows
649 colwidths = []
649 colwidths = []
650 totwidth = -len(separator)
650 totwidth = -len(separator)
651 for col in range(ncols):
651 for col in range(ncols):
652 # Get max column width for this column
652 # Get max column width for this column
653 colwidth = 0
653 colwidth = 0
654 for row in range(nrows):
654 for row in range(nrows):
655 i = array_index(nrows, row, col)
655 i = array_index(nrows, row, col)
656 if i >= size: break
656 if i >= size: break
657 x = items[i]
657 x = items[i]
658 colwidth = max(colwidth, len(x))
658 colwidth = max(colwidth, len(x))
659 colwidths.append(colwidth)
659 colwidths.append(colwidth)
660 totwidth += colwidth + len(separator)
660 totwidth += colwidth + len(separator)
661 if totwidth > displaywidth:
661 if totwidth > displaywidth:
662 break
662 break
663 if totwidth <= displaywidth:
663 if totwidth <= displaywidth:
664 break
664 break
665
665
666 # The smallest number of rows computed and the max widths for each
666 # The smallest number of rows computed and the max widths for each
667 # column has been obtained. Now we just have to format each of the rows.
667 # column has been obtained. Now we just have to format each of the rows.
668 string = ''
668 string = ''
669 for row in range(nrows):
669 for row in range(nrows):
670 texts = []
670 texts = []
671 for col in range(ncols):
671 for col in range(ncols):
672 i = row + nrows*col
672 i = row + nrows*col
673 if i >= size:
673 if i >= size:
674 texts.append('')
674 texts.append('')
675 else:
675 else:
676 texts.append(items[i])
676 texts.append(items[i])
677 while texts and not texts[-1]:
677 while texts and not texts[-1]:
678 del texts[-1]
678 del texts[-1]
679 for col in range(len(texts)):
679 for col in range(len(texts)):
680 texts[col] = texts[col].ljust(colwidths[col])
680 texts[col] = texts[col].ljust(colwidths[col])
681 string += '%s\n' % str(separator.join(texts))
681 string += '%s\n' % str(separator.join(texts))
682 return string
682 return string
683
683
684 def _get_block_plain_text(self, block):
684 def _get_block_plain_text(self, block):
685 """ Given a QTextBlock, return its unformatted text.
685 """ Given a QTextBlock, return its unformatted text.
686 """
686 """
687 cursor = QtGui.QTextCursor(block)
687 cursor = QtGui.QTextCursor(block)
688 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
688 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
689 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
689 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
690 QtGui.QTextCursor.KeepAnchor)
690 QtGui.QTextCursor.KeepAnchor)
691 return str(cursor.selection().toPlainText())
691 return str(cursor.selection().toPlainText())
692
692
693 def _get_cursor(self):
693 def _get_cursor(self):
694 """ Convenience method that returns a cursor for the current position.
694 """ Convenience method that returns a cursor for the current position.
695 """
695 """
696 return self._control.textCursor()
696 return self._control.textCursor()
697
697
698 def _get_end_cursor(self):
698 def _get_end_cursor(self):
699 """ Convenience method that returns a cursor for the last character.
699 """ Convenience method that returns a cursor for the last character.
700 """
700 """
701 cursor = self._control.textCursor()
701 cursor = self._control.textCursor()
702 cursor.movePosition(QtGui.QTextCursor.End)
702 cursor.movePosition(QtGui.QTextCursor.End)
703 return cursor
703 return cursor
704
704
705 def _get_input_buffer_cursor_line(self):
705 def _get_input_buffer_cursor_line(self):
706 """ The text in the line of the input buffer in which the user's cursor
706 """ The text in the line of the input buffer in which the user's cursor
707 rests. Returns a string if there is such a line; otherwise, None.
707 rests. Returns a string if there is such a line; otherwise, None.
708 """
708 """
709 if self._executing:
709 if self._executing:
710 return None
710 return None
711 cursor = self._control.textCursor()
711 cursor = self._control.textCursor()
712 if cursor.position() >= self._prompt_pos:
712 if cursor.position() >= self._prompt_pos:
713 text = self._get_block_plain_text(cursor.block())
713 text = self._get_block_plain_text(cursor.block())
714 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
714 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
715 return text[len(self._prompt):]
715 return text[len(self._prompt):]
716 else:
716 else:
717 return text[len(self._continuation_prompt):]
717 return text[len(self._continuation_prompt):]
718 else:
718 else:
719 return None
719 return None
720
720
721 def _get_prompt_cursor(self):
721 def _get_prompt_cursor(self):
722 """ Convenience method that returns a cursor for the prompt position.
722 """ Convenience method that returns a cursor for the prompt position.
723 """
723 """
724 cursor = self._control.textCursor()
724 cursor = self._control.textCursor()
725 cursor.setPosition(self._prompt_pos)
725 cursor.setPosition(self._prompt_pos)
726 return cursor
726 return cursor
727
727
728 def _get_selection_cursor(self, start, end):
728 def _get_selection_cursor(self, start, end):
729 """ Convenience method that returns a cursor with text selected between
729 """ Convenience method that returns a cursor with text selected between
730 the positions 'start' and 'end'.
730 the positions 'start' and 'end'.
731 """
731 """
732 cursor = self._control.textCursor()
732 cursor = self._control.textCursor()
733 cursor.setPosition(start)
733 cursor.setPosition(start)
734 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
734 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
735 return cursor
735 return cursor
736
736
737 def _get_word_start_cursor(self, position):
737 def _get_word_start_cursor(self, position):
738 """ Find the start of the word to the left the given position. If a
738 """ Find the start of the word to the left the given position. If a
739 sequence of non-word characters precedes the first word, skip over
739 sequence of non-word characters precedes the first word, skip over
740 them. (This emulates the behavior of bash, emacs, etc.)
740 them. (This emulates the behavior of bash, emacs, etc.)
741 """
741 """
742 document = self._control.document()
742 document = self._control.document()
743 position -= 1
743 position -= 1
744 while position >= self._prompt_pos and \
744 while position >= self._prompt_pos and \
745 not document.characterAt(position).isLetterOrNumber():
745 not document.characterAt(position).isLetterOrNumber():
746 position -= 1
746 position -= 1
747 while position >= self._prompt_pos and \
747 while position >= self._prompt_pos and \
748 document.characterAt(position).isLetterOrNumber():
748 document.characterAt(position).isLetterOrNumber():
749 position -= 1
749 position -= 1
750 cursor = self._control.textCursor()
750 cursor = self._control.textCursor()
751 cursor.setPosition(position + 1)
751 cursor.setPosition(position + 1)
752 return cursor
752 return cursor
753
753
754 def _get_word_end_cursor(self, position):
754 def _get_word_end_cursor(self, position):
755 """ Find the end of the word to the right the given position. If a
755 """ Find the end of the word to the right the given position. If a
756 sequence of non-word characters precedes the first word, skip over
756 sequence of non-word characters precedes the first word, skip over
757 them. (This emulates the behavior of bash, emacs, etc.)
757 them. (This emulates the behavior of bash, emacs, etc.)
758 """
758 """
759 document = self._control.document()
759 document = self._control.document()
760 end = self._get_end_cursor().position()
760 end = self._get_end_cursor().position()
761 while position < end and \
761 while position < end and \
762 not document.characterAt(position).isLetterOrNumber():
762 not document.characterAt(position).isLetterOrNumber():
763 position += 1
763 position += 1
764 while position < end and \
764 while position < end and \
765 document.characterAt(position).isLetterOrNumber():
765 document.characterAt(position).isLetterOrNumber():
766 position += 1
766 position += 1
767 cursor = self._control.textCursor()
767 cursor = self._control.textCursor()
768 cursor.setPosition(position)
768 cursor.setPosition(position)
769 return cursor
769 return cursor
770
770
771 def _insert_html(self, cursor, html):
771 def _insert_html(self, cursor, html):
772 """ Insert HTML using the specified cursor in such a way that future
772 """ Insert HTML using the specified cursor in such a way that future
773 formatting is unaffected.
773 formatting is unaffected.
774 """
774 """
775 cursor.beginEditBlock()
775 cursor.beginEditBlock()
776 cursor.insertHtml(html)
776 cursor.insertHtml(html)
777
777
778 # After inserting HTML, the text document "remembers" it's in "html
778 # After inserting HTML, the text document "remembers" it's in "html
779 # mode", which means that subsequent calls adding plain text will result
779 # mode", which means that subsequent calls adding plain text will result
780 # in unwanted formatting, lost tab characters, etc. The following code
780 # in unwanted formatting, lost tab characters, etc. The following code
781 # hacks around this behavior, which I consider to be a bug in Qt.
781 # hacks around this behavior, which I consider to be a bug in Qt.
782 cursor.movePosition(QtGui.QTextCursor.Left,
782 cursor.movePosition(QtGui.QTextCursor.Left,
783 QtGui.QTextCursor.KeepAnchor)
783 QtGui.QTextCursor.KeepAnchor)
784 if cursor.selection().toPlainText() == ' ':
784 if cursor.selection().toPlainText() == ' ':
785 cursor.removeSelectedText()
785 cursor.removeSelectedText()
786 cursor.movePosition(QtGui.QTextCursor.Right)
786 cursor.movePosition(QtGui.QTextCursor.Right)
787 cursor.insertText(' ', QtGui.QTextCharFormat())
787 cursor.insertText(' ', QtGui.QTextCharFormat())
788 cursor.endEditBlock()
788 cursor.endEditBlock()
789
789
790 def _insert_into_buffer(self, text):
790 def _insert_into_buffer(self, text):
791 """ Inserts text into the input buffer at the current cursor position,
791 """ Inserts text into the input buffer at the current cursor position,
792 ensuring that continuation prompts are inserted as necessary.
792 ensuring that continuation prompts are inserted as necessary.
793 """
793 """
794 lines = str(text).splitlines(True)
794 lines = str(text).splitlines(True)
795 if lines:
795 if lines:
796 self._keep_cursor_in_buffer()
796 self._keep_cursor_in_buffer()
797 cursor = self._control.textCursor()
797 cursor = self._control.textCursor()
798 cursor.beginEditBlock()
798 cursor.beginEditBlock()
799 cursor.insertText(lines[0])
799 cursor.insertText(lines[0])
800 for line in lines[1:]:
800 for line in lines[1:]:
801 if self._continuation_prompt_html is None:
801 if self._continuation_prompt_html is None:
802 cursor.insertText(self._continuation_prompt)
802 cursor.insertText(self._continuation_prompt)
803 else:
803 else:
804 self._insert_html(cursor, self._continuation_prompt_html)
804 self._insert_html(cursor, self._continuation_prompt_html)
805 cursor.insertText(line)
805 cursor.insertText(line)
806 cursor.endEditBlock()
806 cursor.endEditBlock()
807 self._control.setTextCursor(cursor)
807 self._control.setTextCursor(cursor)
808
808
809 def _in_buffer(self, position):
809 def _in_buffer(self, position):
810 """ Returns whether the given position is inside the editing region.
810 """ Returns whether the given position is inside the editing region.
811 """
811 """
812 cursor = self._control.textCursor()
812 cursor = self._control.textCursor()
813 cursor.setPosition(position)
813 cursor.setPosition(position)
814 line = cursor.blockNumber()
814 line = cursor.blockNumber()
815 prompt_line = self._get_prompt_cursor().blockNumber()
815 prompt_line = self._get_prompt_cursor().blockNumber()
816 if line == prompt_line:
816 if line == prompt_line:
817 return position >= self._prompt_pos
817 return position >= self._prompt_pos
818 elif line > prompt_line:
818 elif line > prompt_line:
819 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
819 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
820 prompt_pos = cursor.position() + len(self._continuation_prompt)
820 prompt_pos = cursor.position() + len(self._continuation_prompt)
821 return position >= prompt_pos
821 return position >= prompt_pos
822 return False
822 return False
823
823
824 def _keep_cursor_in_buffer(self):
824 def _keep_cursor_in_buffer(self):
825 """ Ensures that the cursor is inside the editing region. Returns
825 """ Ensures that the cursor is inside the editing region. Returns
826 whether the cursor was moved.
826 whether the cursor was moved.
827 """
827 """
828 cursor = self._control.textCursor()
828 cursor = self._control.textCursor()
829 if self._in_buffer(cursor.position()):
829 if self._in_buffer(cursor.position()):
830 return False
830 return False
831 else:
831 else:
832 cursor.movePosition(QtGui.QTextCursor.End)
832 cursor.movePosition(QtGui.QTextCursor.End)
833 self._control.setTextCursor(cursor)
833 self._control.setTextCursor(cursor)
834 return True
834 return True
835
835
836 def _prompt_started(self):
836 def _prompt_started(self):
837 """ Called immediately after a new prompt is displayed.
837 """ Called immediately after a new prompt is displayed.
838 """
838 """
839 # Temporarily disable the maximum block count to permit undo/redo and
839 # Temporarily disable the maximum block count to permit undo/redo and
840 # to ensure that the prompt position does not change due to truncation.
840 # to ensure that the prompt position does not change due to truncation.
841 self._control.document().setMaximumBlockCount(0)
841 self._control.document().setMaximumBlockCount(0)
842 self._control.setUndoRedoEnabled(True)
842 self._control.setUndoRedoEnabled(True)
843
843
844 self._control.setReadOnly(False)
844 self._control.setReadOnly(False)
845 self._control.moveCursor(QtGui.QTextCursor.End)
845 self._control.moveCursor(QtGui.QTextCursor.End)
846
846
847 self._executing = False
847 self._executing = False
848 self._prompt_started_hook()
848 self._prompt_started_hook()
849
849
850 def _prompt_finished(self):
850 def _prompt_finished(self):
851 """ Called immediately after a prompt is finished, i.e. when some input
851 """ Called immediately after a prompt is finished, i.e. when some input
852 will be processed and a new prompt displayed.
852 will be processed and a new prompt displayed.
853 """
853 """
854 self._control.setUndoRedoEnabled(False)
854 self._control.setUndoRedoEnabled(False)
855 self._control.setReadOnly(True)
855 self._control.setReadOnly(True)
856 self._prompt_finished_hook()
856 self._prompt_finished_hook()
857
857
858 def _readline(self, prompt='', callback=None):
858 def _readline(self, prompt='', callback=None):
859 """ Reads one line of input from the user.
859 """ Reads one line of input from the user.
860
860
861 Parameters
861 Parameters
862 ----------
862 ----------
863 prompt : str, optional
863 prompt : str, optional
864 The prompt to print before reading the line.
864 The prompt to print before reading the line.
865
865
866 callback : callable, optional
866 callback : callable, optional
867 A callback to execute with the read line. If not specified, input is
867 A callback to execute with the read line. If not specified, input is
868 read *synchronously* and this method does not return until it has
868 read *synchronously* and this method does not return until it has
869 been read.
869 been read.
870
870
871 Returns
871 Returns
872 -------
872 -------
873 If a callback is specified, returns nothing. Otherwise, returns the
873 If a callback is specified, returns nothing. Otherwise, returns the
874 input string with the trailing newline stripped.
874 input string with the trailing newline stripped.
875 """
875 """
876 if self._reading:
876 if self._reading:
877 raise RuntimeError('Cannot read a line. Widget is already reading.')
877 raise RuntimeError('Cannot read a line. Widget is already reading.')
878
878
879 if not callback and not self.isVisible():
879 if not callback and not self.isVisible():
880 # If the user cannot see the widget, this function cannot return.
880 # If the user cannot see the widget, this function cannot return.
881 raise RuntimeError('Cannot synchronously read a line if the widget'
881 raise RuntimeError('Cannot synchronously read a line if the widget'
882 'is not visible!')
882 'is not visible!')
883
883
884 self._reading = True
884 self._reading = True
885 self._show_prompt(prompt, newline=False)
885 self._show_prompt(prompt, newline=False)
886
886
887 if callback is None:
887 if callback is None:
888 self._reading_callback = None
888 self._reading_callback = None
889 while self._reading:
889 while self._reading:
890 QtCore.QCoreApplication.processEvents()
890 QtCore.QCoreApplication.processEvents()
891 return self.input_buffer.rstrip('\n')
891 return self.input_buffer.rstrip('\n')
892
892
893 else:
893 else:
894 self._reading_callback = lambda: \
894 self._reading_callback = lambda: \
895 callback(self.input_buffer.rstrip('\n'))
895 callback(self.input_buffer.rstrip('\n'))
896
896
897 def _reset(self):
897 def _reset(self):
898 """ Clears the console and resets internal state variables.
898 """ Clears the console and resets internal state variables.
899 """
899 """
900 self._control.clear()
900 self._control.clear()
901 self._executing = self._reading = False
901 self._executing = self._reading = False
902
902
903 def _set_continuation_prompt(self, prompt, html=False):
903 def _set_continuation_prompt(self, prompt, html=False):
904 """ Sets the continuation prompt.
904 """ Sets the continuation prompt.
905
905
906 Parameters
906 Parameters
907 ----------
907 ----------
908 prompt : str
908 prompt : str
909 The prompt to show when more input is needed.
909 The prompt to show when more input is needed.
910
910
911 html : bool, optional (default False)
911 html : bool, optional (default False)
912 If set, the prompt will be inserted as formatted HTML. Otherwise,
912 If set, the prompt will be inserted as formatted HTML. Otherwise,
913 the prompt will be treated as plain text, though ANSI color codes
913 the prompt will be treated as plain text, though ANSI color codes
914 will be handled.
914 will be handled.
915 """
915 """
916 if html:
916 if html:
917 self._continuation_prompt_html = prompt
917 self._continuation_prompt_html = prompt
918 else:
918 else:
919 self._continuation_prompt = prompt
919 self._continuation_prompt = prompt
920 self._continuation_prompt_html = None
920 self._continuation_prompt_html = None
921
921
922 def _set_cursor(self, cursor):
922 def _set_cursor(self, cursor):
923 """ Convenience method to set the current cursor.
923 """ Convenience method to set the current cursor.
924 """
924 """
925 self._control.setTextCursor(cursor)
925 self._control.setTextCursor(cursor)
926
926
927 def _set_position(self, position):
927 def _set_position(self, position):
928 """ Convenience method to set the position of the cursor.
928 """ Convenience method to set the position of the cursor.
929 """
929 """
930 cursor = self._control.textCursor()
930 cursor = self._control.textCursor()
931 cursor.setPosition(position)
931 cursor.setPosition(position)
932 self._control.setTextCursor(cursor)
932 self._control.setTextCursor(cursor)
933
933
934 def _set_selection(self, start, end):
934 def _set_selection(self, start, end):
935 """ Convenience method to set the current selected text.
935 """ Convenience method to set the current selected text.
936 """
936 """
937 self._control.setTextCursor(self._get_selection_cursor(start, end))
937 self._control.setTextCursor(self._get_selection_cursor(start, end))
938
938
939 def _show_context_menu(self, pos):
939 def _show_context_menu(self, pos):
940 """ Shows a context menu at the given QPoint (in widget coordinates).
940 """ Shows a context menu at the given QPoint (in widget coordinates).
941 """
941 """
942 menu = QtGui.QMenu()
942 menu = QtGui.QMenu()
943
943
944 copy_action = menu.addAction('Copy', self.copy)
944 copy_action = menu.addAction('Copy', self.copy)
945 copy_action.setEnabled(self._get_cursor().hasSelection())
945 copy_action.setEnabled(self._get_cursor().hasSelection())
946 copy_action.setShortcut(QtGui.QKeySequence.Copy)
946 copy_action.setShortcut(QtGui.QKeySequence.Copy)
947
947
948 paste_action = menu.addAction('Paste', self.paste)
948 paste_action = menu.addAction('Paste', self.paste)
949 paste_action.setEnabled(self.can_paste())
949 paste_action.setEnabled(self.can_paste())
950 paste_action.setShortcut(QtGui.QKeySequence.Paste)
950 paste_action.setShortcut(QtGui.QKeySequence.Paste)
951
951
952 menu.addSeparator()
952 menu.addSeparator()
953 menu.addAction('Select All', self.select_all)
953 menu.addAction('Select All', self.select_all)
954
954
955 menu.exec_(self._control.mapToGlobal(pos))
955 menu.exec_(self._control.mapToGlobal(pos))
956
956
957 def _show_prompt(self, prompt=None, html=False, newline=True):
957 def _show_prompt(self, prompt=None, html=False, newline=True):
958 """ Writes a new prompt at the end of the buffer.
958 """ Writes a new prompt at the end of the buffer.
959
959
960 Parameters
960 Parameters
961 ----------
961 ----------
962 prompt : str, optional
962 prompt : str, optional
963 The prompt to show. If not specified, the previous prompt is used.
963 The prompt to show. If not specified, the previous prompt is used.
964
964
965 html : bool, optional (default False)
965 html : bool, optional (default False)
966 Only relevant when a prompt is specified. If set, the prompt will
966 Only relevant when a prompt is specified. If set, the prompt will
967 be inserted as formatted HTML. Otherwise, the prompt will be treated
967 be inserted as formatted HTML. Otherwise, the prompt will be treated
968 as plain text, though ANSI color codes will be handled.
968 as plain text, though ANSI color codes will be handled.
969
969
970 newline : bool, optional (default True)
970 newline : bool, optional (default True)
971 If set, a new line will be written before showing the prompt if
971 If set, a new line will be written before showing the prompt if
972 there is not already a newline at the end of the buffer.
972 there is not already a newline at the end of the buffer.
973 """
973 """
974 # Insert a preliminary newline, if necessary.
974 # Insert a preliminary newline, if necessary.
975 if newline:
975 if newline:
976 cursor = self._get_end_cursor()
976 cursor = self._get_end_cursor()
977 if cursor.position() > 0:
977 if cursor.position() > 0:
978 cursor.movePosition(QtGui.QTextCursor.Left,
978 cursor.movePosition(QtGui.QTextCursor.Left,
979 QtGui.QTextCursor.KeepAnchor)
979 QtGui.QTextCursor.KeepAnchor)
980 if str(cursor.selection().toPlainText()) != '\n':
980 if str(cursor.selection().toPlainText()) != '\n':
981 self._append_plain_text('\n')
981 self._append_plain_text('\n')
982
982
983 # Write the prompt.
983 # Write the prompt.
984 if prompt is None:
984 if prompt is None:
985 if self._prompt_html is None:
985 if self._prompt_html is None:
986 self._append_plain_text(self._prompt)
986 self._append_plain_text(self._prompt)
987 else:
987 else:
988 self._append_html(self._prompt_html)
988 self._append_html(self._prompt_html)
989 else:
989 else:
990 if html:
990 if html:
991 self._prompt = self._append_html_fetching_plain_text(prompt)
991 self._prompt = self._append_html_fetching_plain_text(prompt)
992 self._prompt_html = prompt
992 self._prompt_html = prompt
993 else:
993 else:
994 self._append_plain_text(prompt)
994 self._append_plain_text(prompt)
995 self._prompt = prompt
995 self._prompt = prompt
996 self._prompt_html = None
996 self._prompt_html = None
997
997
998 self._prompt_pos = self._get_end_cursor().position()
998 self._prompt_pos = self._get_end_cursor().position()
999 self._prompt_started()
999 self._prompt_started()
1000
1000
1001 def _show_continuation_prompt(self):
1001 def _show_continuation_prompt(self):
1002 """ Writes a new continuation prompt at the end of the buffer.
1002 """ Writes a new continuation prompt at the end of the buffer.
1003 """
1003 """
1004 if self._continuation_prompt_html is None:
1004 if self._continuation_prompt_html is None:
1005 self._append_plain_text(self._continuation_prompt)
1005 self._append_plain_text(self._continuation_prompt)
1006 else:
1006 else:
1007 self._continuation_prompt = self._append_html_fetching_plain_text(
1007 self._continuation_prompt = self._append_html_fetching_plain_text(
1008 self._continuation_prompt_html)
1008 self._continuation_prompt_html)
1009
1009
1010 self._prompt_started()
1010 self._prompt_started()
1011
1011
1012
1012
1013 class HistoryConsoleWidget(ConsoleWidget):
1013 class HistoryConsoleWidget(ConsoleWidget):
1014 """ A ConsoleWidget that keeps a history of the commands that have been
1014 """ A ConsoleWidget that keeps a history of the commands that have been
1015 executed.
1015 executed.
1016 """
1016 """
1017
1017
1018 #---------------------------------------------------------------------------
1018 #---------------------------------------------------------------------------
1019 # 'object' interface
1019 # 'object' interface
1020 #---------------------------------------------------------------------------
1020 #---------------------------------------------------------------------------
1021
1021
1022 def __init__(self, *args, **kw):
1022 def __init__(self, *args, **kw):
1023 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1023 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1024 self._history = []
1024 self._history = []
1025 self._history_index = 0
1025 self._history_index = 0
1026
1026
1027 #---------------------------------------------------------------------------
1027 #---------------------------------------------------------------------------
1028 # 'ConsoleWidget' public interface
1028 # 'ConsoleWidget' public interface
1029 #---------------------------------------------------------------------------
1029 #---------------------------------------------------------------------------
1030
1030
1031 def execute(self, source=None, hidden=False, interactive=False):
1031 def execute(self, source=None, hidden=False, interactive=False):
1032 """ Reimplemented to the store history.
1032 """ Reimplemented to the store history.
1033 """
1033 """
1034 if not hidden:
1034 if not hidden:
1035 history = self.input_buffer if source is None else source
1035 history = self.input_buffer if source is None else source
1036
1036
1037 executed = super(HistoryConsoleWidget, self).execute(
1037 executed = super(HistoryConsoleWidget, self).execute(
1038 source, hidden, interactive)
1038 source, hidden, interactive)
1039
1039
1040 if executed and not hidden:
1040 if executed and not hidden:
1041 self._history.append(history.rstrip())
1041 self._history.append(history.rstrip())
1042 self._history_index = len(self._history)
1042 self._history_index = len(self._history)
1043
1043
1044 return executed
1044 return executed
1045
1045
1046 #---------------------------------------------------------------------------
1046 #---------------------------------------------------------------------------
1047 # 'ConsoleWidget' abstract interface
1047 # 'ConsoleWidget' abstract interface
1048 #---------------------------------------------------------------------------
1048 #---------------------------------------------------------------------------
1049
1049
1050 def _up_pressed(self):
1050 def _up_pressed(self):
1051 """ Called when the up key is pressed. Returns whether to continue
1051 """ Called when the up key is pressed. Returns whether to continue
1052 processing the event.
1052 processing the event.
1053 """
1053 """
1054 prompt_cursor = self._get_prompt_cursor()
1054 prompt_cursor = self._get_prompt_cursor()
1055 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1055 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1056 self.history_previous()
1056 self.history_previous()
1057
1057
1058 # Go to the first line of prompt for seemless history scrolling.
1058 # Go to the first line of prompt for seemless history scrolling.
1059 cursor = self._get_prompt_cursor()
1059 cursor = self._get_prompt_cursor()
1060 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1060 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1061 self._set_cursor(cursor)
1061 self._set_cursor(cursor)
1062
1062
1063 return False
1063 return False
1064 return True
1064 return True
1065
1065
1066 def _down_pressed(self):
1066 def _down_pressed(self):
1067 """ Called when the down key is pressed. Returns whether to continue
1067 """ Called when the down key is pressed. Returns whether to continue
1068 processing the event.
1068 processing the event.
1069 """
1069 """
1070 end_cursor = self._get_end_cursor()
1070 end_cursor = self._get_end_cursor()
1071 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1071 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1072 self.history_next()
1072 self.history_next()
1073 return False
1073 return False
1074 return True
1074 return True
1075
1075
1076 #---------------------------------------------------------------------------
1076 #---------------------------------------------------------------------------
1077 # 'HistoryConsoleWidget' interface
1077 # 'HistoryConsoleWidget' interface
1078 #---------------------------------------------------------------------------
1078 #---------------------------------------------------------------------------
1079
1079
1080 def history_previous(self):
1080 def history_previous(self):
1081 """ If possible, set the input buffer to the previous item in the
1081 """ If possible, set the input buffer to the previous item in the
1082 history.
1082 history.
1083 """
1083 """
1084 if self._history_index > 0:
1084 if self._history_index > 0:
1085 self._history_index -= 1
1085 self._history_index -= 1
1086 self.input_buffer = self._history[self._history_index]
1086 self.input_buffer = self._history[self._history_index]
1087
1087
1088 def history_next(self):
1088 def history_next(self):
1089 """ Set the input buffer to the next item in the history, or a blank
1089 """ Set the input buffer to the next item in the history, or a blank
1090 line if there is no subsequent item.
1090 line if there is no subsequent item.
1091 """
1091 """
1092 if self._history_index < len(self._history):
1092 if self._history_index < len(self._history):
1093 self._history_index += 1
1093 self._history_index += 1
1094 if self._history_index < len(self._history):
1094 if self._history_index < len(self._history):
1095 self.input_buffer = self._history[self._history_index]
1095 self.input_buffer = self._history[self._history_index]
1096 else:
1096 else:
1097 self.input_buffer = ''
1097 self.input_buffer = ''
@@ -1,340 +1,344 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 setFormat(self, start, count, format):
57 def setFormat(self, start, count, format):
58 """ Reimplemented to highlight selectively.
58 """ Reimplemented to highlight selectively.
59 """
59 """
60 start += self._current_offset
60 start += self._current_offset
61 PygmentsHighlighter.setFormat(self, start, count, format)
61 PygmentsHighlighter.setFormat(self, start, count, format)
62
62
63
63
64 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
64 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
65 """ A Qt frontend for a generic Python kernel.
65 """ A Qt frontend for a generic Python kernel.
66 """
66 """
67
67
68 # Emitted when an 'execute_reply' is received from the kernel.
68 # Emitted when an 'execute_reply' has been received from the kernel and
69 # processed by the FrontendWidget.
69 executed = QtCore.pyqtSignal(object)
70 executed = QtCore.pyqtSignal(object)
70
71
71 #---------------------------------------------------------------------------
72 #---------------------------------------------------------------------------
72 # 'object' interface
73 # 'object' interface
73 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
74
75
75 def __init__(self, *args, **kw):
76 def __init__(self, *args, **kw):
76 super(FrontendWidget, self).__init__(*args, **kw)
77 super(FrontendWidget, self).__init__(*args, **kw)
77
78
78 # FrontendWidget protected variables.
79 # FrontendWidget protected variables.
79 self._call_tip_widget = CallTipWidget(self._control)
80 self._call_tip_widget = CallTipWidget(self._control)
80 self._completion_lexer = CompletionLexer(PythonLexer())
81 self._completion_lexer = CompletionLexer(PythonLexer())
81 self._hidden = True
82 self._hidden = True
82 self._highlighter = FrontendHighlighter(self)
83 self._highlighter = FrontendHighlighter(self)
83 self._input_splitter = InputSplitter(input_mode='replace')
84 self._input_splitter = InputSplitter(input_mode='replace')
84 self._kernel_manager = None
85 self._kernel_manager = None
85
86
86 # Configure the ConsoleWidget.
87 # Configure the ConsoleWidget.
87 self.tab_width = 4
88 self.tab_width = 4
88 self._set_continuation_prompt('... ')
89 self._set_continuation_prompt('... ')
89
90
90 # Connect signal handlers.
91 # Connect signal handlers.
91 document = self._control.document()
92 document = self._control.document()
92 document.contentsChange.connect(self._document_contents_change)
93 document.contentsChange.connect(self._document_contents_change)
93
94
94 #---------------------------------------------------------------------------
95 #---------------------------------------------------------------------------
95 # 'ConsoleWidget' abstract interface
96 # 'ConsoleWidget' abstract interface
96 #---------------------------------------------------------------------------
97 #---------------------------------------------------------------------------
97
98
98 def _is_complete(self, source, interactive):
99 def _is_complete(self, source, interactive):
99 """ Returns whether 'source' can be completely processed and a new
100 """ Returns whether 'source' can be completely processed and a new
100 prompt created. When triggered by an Enter/Return key press,
101 prompt created. When triggered by an Enter/Return key press,
101 'interactive' is True; otherwise, it is False.
102 'interactive' is True; otherwise, it is False.
102 """
103 """
103 complete = self._input_splitter.push(source.expandtabs(4))
104 complete = self._input_splitter.push(source.expandtabs(4))
104 if interactive:
105 if interactive:
105 complete = not self._input_splitter.push_accepts_more()
106 complete = not self._input_splitter.push_accepts_more()
106 return complete
107 return complete
107
108
108 def _execute(self, source, hidden):
109 def _execute(self, source, hidden):
109 """ Execute 'source'. If 'hidden', do not show any output.
110 """ Execute 'source'. If 'hidden', do not show any output.
110 """
111 """
111 self.kernel_manager.xreq_channel.execute(source)
112 self.kernel_manager.xreq_channel.execute(source)
112 self._hidden = hidden
113 self._hidden = hidden
113
114
114 def _execute_interrupt(self):
115 def _execute_interrupt(self):
115 """ Attempts to stop execution. Returns whether this method has an
116 """ Attempts to stop execution. Returns whether this method has an
116 implementation.
117 implementation.
117 """
118 """
118 self._interrupt_kernel()
119 self._interrupt_kernel()
119 return True
120 return True
120
121
121 def _prompt_started_hook(self):
122 def _prompt_started_hook(self):
122 """ Called immediately after a new prompt is displayed.
123 """ Called immediately after a new prompt is displayed.
123 """
124 """
124 if not self._reading:
125 if not self._reading:
125 self._highlighter.highlighting_on = True
126 self._highlighter.highlighting_on = True
126
127
127 # Auto-indent if this is a continuation prompt.
128 # Auto-indent if this is a continuation prompt.
128 if self._get_prompt_cursor().blockNumber() != \
129 if self._get_prompt_cursor().blockNumber() != \
129 self._get_end_cursor().blockNumber():
130 self._get_end_cursor().blockNumber():
130 spaces = self._input_splitter.indent_spaces
131 spaces = self._input_splitter.indent_spaces
131 self._append_plain_text('\t' * (spaces / self.tab_width))
132 self._append_plain_text('\t' * (spaces / self.tab_width))
132 self._append_plain_text(' ' * (spaces % self.tab_width))
133 self._append_plain_text(' ' * (spaces % self.tab_width))
133
134
134 def _prompt_finished_hook(self):
135 def _prompt_finished_hook(self):
135 """ Called immediately after a prompt is finished, i.e. when some input
136 """ Called immediately after a prompt is finished, i.e. when some input
136 will be processed and a new prompt displayed.
137 will be processed and a new prompt displayed.
137 """
138 """
138 if not self._reading:
139 if not self._reading:
139 self._highlighter.highlighting_on = False
140 self._highlighter.highlighting_on = False
140
141
141 def _tab_pressed(self):
142 def _tab_pressed(self):
142 """ Called when the tab key is pressed. Returns whether to continue
143 """ Called when the tab key is pressed. Returns whether to continue
143 processing the event.
144 processing the event.
144 """
145 """
145 self._keep_cursor_in_buffer()
146 self._keep_cursor_in_buffer()
146 cursor = self._get_cursor()
147 cursor = self._get_cursor()
147 return not self._complete()
148 return not self._complete()
148
149
149 #---------------------------------------------------------------------------
150 #---------------------------------------------------------------------------
150 # 'BaseFrontendMixin' abstract interface
151 # 'BaseFrontendMixin' abstract interface
151 #---------------------------------------------------------------------------
152 #---------------------------------------------------------------------------
152
153
153 def _handle_complete_reply(self, rep):
154 def _handle_complete_reply(self, rep):
154 """ Handle replies for tab completion.
155 """ Handle replies for tab completion.
155 """
156 """
156 cursor = self._get_cursor()
157 cursor = self._get_cursor()
157 if rep['parent_header']['msg_id'] == self._complete_id and \
158 if rep['parent_header']['msg_id'] == self._complete_id and \
158 cursor.position() == self._complete_pos:
159 cursor.position() == self._complete_pos:
159 text = '.'.join(self._get_context())
160 text = '.'.join(self._get_context())
160 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
161 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
161 self._complete_with_items(cursor, rep['content']['matches'])
162 self._complete_with_items(cursor, rep['content']['matches'])
162
163
163 def _handle_execute_reply(self, msg):
164 def _handle_execute_reply(self, msg):
164 """ Handles replies for code execution.
165 """ Handles replies for code execution.
165 """
166 """
166 if not self._hidden:
167 if not self._hidden:
167 # Make sure that all output from the SUB channel has been processed
168 # Make sure that all output from the SUB channel has been processed
168 # before writing a new prompt.
169 # before writing a new prompt.
169 self.kernel_manager.sub_channel.flush()
170 self.kernel_manager.sub_channel.flush()
170
171
171 content = msg['content']
172 content = msg['content']
172 status = content['status']
173 status = content['status']
173 if status == 'ok':
174 if status == 'ok':
174 self._process_execute_ok(msg)
175 self._process_execute_ok(msg)
175 elif status == 'error':
176 elif status == 'error':
176 self._process_execute_error(msg)
177 self._process_execute_error(msg)
177 elif status == 'abort':
178 elif status == 'abort':
178 self._process_execute_abort(msg)
179 self._process_execute_abort(msg)
179
180
180 self._hidden = True
181 self._hidden = True
181 self._show_interpreter_prompt()
182 self._show_interpreter_prompt()
182 self.executed.emit(msg)
183 self.executed.emit(msg)
183
184
184 def _handle_input_request(self, msg):
185 def _handle_input_request(self, msg):
185 """ Handle requests for raw_input.
186 """ Handle requests for raw_input.
186 """
187 """
188 if self._hidden:
189 raise RuntimeError('Request for raw input during hidden execution.')
190
187 # Make sure that all output from the SUB channel has been processed
191 # Make sure that all output from the SUB channel has been processed
188 # before entering readline mode.
192 # before entering readline mode.
189 self.kernel_manager.sub_channel.flush()
193 self.kernel_manager.sub_channel.flush()
190
194
191 def callback(line):
195 def callback(line):
192 self.kernel_manager.rep_channel.input(line)
196 self.kernel_manager.rep_channel.input(line)
193 self._readline(msg['content']['prompt'], callback=callback)
197 self._readline(msg['content']['prompt'], callback=callback)
194
198
195 def _handle_object_info_reply(self, rep):
199 def _handle_object_info_reply(self, rep):
196 """ Handle replies for call tips.
200 """ Handle replies for call tips.
197 """
201 """
198 cursor = self._get_cursor()
202 cursor = self._get_cursor()
199 if rep['parent_header']['msg_id'] == self._call_tip_id and \
203 if rep['parent_header']['msg_id'] == self._call_tip_id and \
200 cursor.position() == self._call_tip_pos:
204 cursor.position() == self._call_tip_pos:
201 doc = rep['content']['docstring']
205 doc = rep['content']['docstring']
202 if doc:
206 if doc:
203 self._call_tip_widget.show_docstring(doc)
207 self._call_tip_widget.show_docstring(doc)
204
208
205 def _handle_pyout(self, msg):
209 def _handle_pyout(self, msg):
206 """ Handle display hook output.
210 """ Handle display hook output.
207 """
211 """
208 self._append_plain_text(msg['content']['data'] + '\n')
212 self._append_plain_text(msg['content']['data'] + '\n')
209
213
210 def _handle_stream(self, msg):
214 def _handle_stream(self, msg):
211 """ Handle stdout, stderr, and stdin.
215 """ Handle stdout, stderr, and stdin.
212 """
216 """
213 self._append_plain_text(msg['content']['data'])
217 self._append_plain_text(msg['content']['data'])
214 self._control.moveCursor(QtGui.QTextCursor.End)
218 self._control.moveCursor(QtGui.QTextCursor.End)
215
219
216 def _started_channels(self):
220 def _started_channels(self):
217 """ Called when the KernelManager channels have started listening or
221 """ Called when the KernelManager channels have started listening or
218 when the frontend is assigned an already listening KernelManager.
222 when the frontend is assigned an already listening KernelManager.
219 """
223 """
220 self._reset()
224 self._reset()
221 self._append_plain_text(self._get_banner())
225 self._append_plain_text(self._get_banner())
222 self._show_interpreter_prompt()
226 self._show_interpreter_prompt()
223
227
224 def _stopped_channels(self):
228 def _stopped_channels(self):
225 """ Called when the KernelManager channels have stopped listening or
229 """ Called when the KernelManager channels have stopped listening or
226 when a listening KernelManager is removed from the frontend.
230 when a listening KernelManager is removed from the frontend.
227 """
231 """
228 # FIXME: Print a message here?
232 # FIXME: Print a message here?
229 pass
233 pass
230
234
231 #---------------------------------------------------------------------------
235 #---------------------------------------------------------------------------
232 # 'FrontendWidget' interface
236 # 'FrontendWidget' interface
233 #---------------------------------------------------------------------------
237 #---------------------------------------------------------------------------
234
238
235 def execute_file(self, path, hidden=False):
239 def execute_file(self, path, hidden=False):
236 """ Attempts to execute file with 'path'. If 'hidden', no output is
240 """ Attempts to execute file with 'path'. If 'hidden', no output is
237 shown.
241 shown.
238 """
242 """
239 self.execute('execfile("%s")' % path, hidden=hidden)
243 self.execute('execfile("%s")' % path, hidden=hidden)
240
244
241 #---------------------------------------------------------------------------
245 #---------------------------------------------------------------------------
242 # 'FrontendWidget' protected interface
246 # 'FrontendWidget' protected interface
243 #---------------------------------------------------------------------------
247 #---------------------------------------------------------------------------
244
248
245 def _call_tip(self):
249 def _call_tip(self):
246 """ Shows a call tip, if appropriate, at the current cursor location.
250 """ Shows a call tip, if appropriate, at the current cursor location.
247 """
251 """
248 # Decide if it makes sense to show a call tip
252 # Decide if it makes sense to show a call tip
249 cursor = self._get_cursor()
253 cursor = self._get_cursor()
250 cursor.movePosition(QtGui.QTextCursor.Left)
254 cursor.movePosition(QtGui.QTextCursor.Left)
251 document = self._control.document()
255 document = self._control.document()
252 if document.characterAt(cursor.position()).toAscii() != '(':
256 if document.characterAt(cursor.position()).toAscii() != '(':
253 return False
257 return False
254 context = self._get_context(cursor)
258 context = self._get_context(cursor)
255 if not context:
259 if not context:
256 return False
260 return False
257
261
258 # Send the metadata request to the kernel
262 # Send the metadata request to the kernel
259 name = '.'.join(context)
263 name = '.'.join(context)
260 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
264 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
261 self._call_tip_pos = self._get_cursor().position()
265 self._call_tip_pos = self._get_cursor().position()
262 return True
266 return True
263
267
264 def _complete(self):
268 def _complete(self):
265 """ Performs completion at the current cursor location.
269 """ Performs completion at the current cursor location.
266 """
270 """
267 # Decide if it makes sense to do completion
271 # Decide if it makes sense to do completion
268 context = self._get_context()
272 context = self._get_context()
269 if not context:
273 if not context:
270 return False
274 return False
271
275
272 # Send the completion request to the kernel
276 # Send the completion request to the kernel
273 text = '.'.join(context)
277 text = '.'.join(context)
274 self._complete_id = self.kernel_manager.xreq_channel.complete(
278 self._complete_id = self.kernel_manager.xreq_channel.complete(
275 text, self._get_input_buffer_cursor_line(), self.input_buffer)
279 text, self._get_input_buffer_cursor_line(), self.input_buffer)
276 self._complete_pos = self._get_cursor().position()
280 self._complete_pos = self._get_cursor().position()
277 return True
281 return True
278
282
279 def _get_banner(self):
283 def _get_banner(self):
280 """ Gets a banner to display at the beginning of a session.
284 """ Gets a banner to display at the beginning of a session.
281 """
285 """
282 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
286 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
283 '"license" for more information.'
287 '"license" for more information.'
284 return banner % (sys.version, sys.platform)
288 return banner % (sys.version, sys.platform)
285
289
286 def _get_context(self, cursor=None):
290 def _get_context(self, cursor=None):
287 """ Gets the context at the current cursor location.
291 """ Gets the context at the current cursor location.
288 """
292 """
289 if cursor is None:
293 if cursor is None:
290 cursor = self._get_cursor()
294 cursor = self._get_cursor()
291 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
295 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
292 QtGui.QTextCursor.KeepAnchor)
296 QtGui.QTextCursor.KeepAnchor)
293 text = str(cursor.selection().toPlainText())
297 text = str(cursor.selection().toPlainText())
294 return self._completion_lexer.get_context(text)
298 return self._completion_lexer.get_context(text)
295
299
296 def _interrupt_kernel(self):
300 def _interrupt_kernel(self):
297 """ Attempts to the interrupt the kernel.
301 """ Attempts to the interrupt the kernel.
298 """
302 """
299 if self.kernel_manager.has_kernel:
303 if self.kernel_manager.has_kernel:
300 self.kernel_manager.signal_kernel(signal.SIGINT)
304 self.kernel_manager.signal_kernel(signal.SIGINT)
301 else:
305 else:
302 self._append_plain_text('Kernel process is either remote or '
306 self._append_plain_text('Kernel process is either remote or '
303 'unspecified. Cannot interrupt.\n')
307 'unspecified. Cannot interrupt.\n')
304
308
305 def _process_execute_abort(self, msg):
309 def _process_execute_abort(self, msg):
306 """ Process a reply for an aborted execution request.
310 """ Process a reply for an aborted execution request.
307 """
311 """
308 self._append_plain_text("ERROR: execution aborted\n")
312 self._append_plain_text("ERROR: execution aborted\n")
309
313
310 def _process_execute_error(self, msg):
314 def _process_execute_error(self, msg):
311 """ Process a reply for an execution request that resulted in an error.
315 """ Process a reply for an execution request that resulted in an error.
312 """
316 """
313 content = msg['content']
317 content = msg['content']
314 traceback = ''.join(content['traceback'])
318 traceback = ''.join(content['traceback'])
315 self._append_plain_text(traceback)
319 self._append_plain_text(traceback)
316
320
317 def _process_execute_ok(self, msg):
321 def _process_execute_ok(self, msg):
318 """ Process a reply for a successful execution equest.
322 """ Process a reply for a successful execution equest.
319 """
323 """
320 # The basic FrontendWidget doesn't handle payloads, as they are a
324 # The basic FrontendWidget doesn't handle payloads, as they are a
321 # mechanism for going beyond the standard Python interpreter model.
325 # mechanism for going beyond the standard Python interpreter model.
322 pass
326 pass
323
327
324 def _show_interpreter_prompt(self):
328 def _show_interpreter_prompt(self):
325 """ Shows a prompt for the interpreter.
329 """ Shows a prompt for the interpreter.
326 """
330 """
327 self._show_prompt('>>> ')
331 self._show_prompt('>>> ')
328
332
329 #------ Signal handlers ----------------------------------------------------
333 #------ Signal handlers ----------------------------------------------------
330
334
331 def _document_contents_change(self, position, removed, added):
335 def _document_contents_change(self, position, removed, added):
332 """ Called whenever the document's content changes. Display a call tip
336 """ Called whenever the document's content changes. Display a call tip
333 if appropriate.
337 if appropriate.
334 """
338 """
335 # Calculate where the cursor should be *after* the change:
339 # Calculate where the cursor should be *after* the change:
336 position += added
340 position += added
337
341
338 document = self._control.document()
342 document = self._control.document()
339 if position == self._get_cursor().position():
343 if position == self._get_cursor().position():
340 self._call_tip()
344 self._call_tip()
General Comments 0
You need to be logged in to leave comments. Login now