##// END OF EJS Templates
* ConsoleWidget now has better support for non-GUI tab completion. Multiple matches are formatted into columns....
epatters -
Show More
@@ -0,0 +1,160 b''
1 """Return compact set of columns as a string with newlines for an
2 array of strings.
3
4 Adapted from the routine of the same name inside cmd.py
5
6 Author: Rocky Bernstein.
7 License: MIT Open Source License.
8 """
9
10 import types
11
12 def columnize(array, displaywidth=80, colsep = ' ',
13 arrange_vertical=True, ljust=True, lineprefix=''):
14 """Return a list of strings as a compact set of columns arranged
15 horizontally or vertically.
16
17 For example, for a line width of 4 characters (arranged vertically):
18 ['1', '2,', '3', '4'] => '1 3\n2 4\n'
19
20 or arranged horizontally:
21 ['1', '2,', '3', '4'] => '1 2\n3 4\n'
22
23 Each column is only as wide as necessary. By default, columns are
24 separated by two spaces - one was not legible enough. Set "colsep"
25 to adjust the string separate columns. Set `displaywidth' to set
26 the line width.
27
28 Normally, consecutive items go down from the top to bottom from
29 the left-most column to the right-most. If "arrange_vertical" is
30 set false, consecutive items will go across, left to right, top to
31 bottom."""
32 if not isinstance(array, list) and not isinstance(array, tuple):
33 raise TypeError, (
34 'array needs to be an instance of a list or a tuple')
35
36 array = [str(i) for i in array]
37
38 # Some degenerate cases
39 size = len(array)
40 if 0 == size:
41 return "<empty>\n"
42 elif size == 1:
43 return '%s\n' % str(array[0])
44
45 displaywidth = max(4, displaywidth - len(lineprefix))
46 if arrange_vertical:
47 array_index = lambda nrows, row, col: nrows*col + row
48 # Try every row count from 1 upwards
49 for nrows in range(1, size):
50 ncols = (size+nrows-1) // nrows
51 colwidths = []
52 totwidth = -len(colsep)
53 for col in range(ncols):
54 # get max column width for this column
55 colwidth = 0
56 for row in range(nrows):
57 i = array_index(nrows, row, col)
58 if i >= size: break
59 x = array[i]
60 colwidth = max(colwidth, len(x))
61 pass
62 colwidths.append(colwidth)
63 totwidth += colwidth + len(colsep)
64 if totwidth > displaywidth:
65 break
66 pass
67 if totwidth <= displaywidth:
68 break
69 pass
70 # The smallest number of rows computed and the
71 # max widths for each column has been obtained.
72 # Now we just have to format each of the
73 # rows.
74 s = ''
75 for row in range(nrows):
76 texts = []
77 for col in range(ncols):
78 i = row + nrows*col
79 if i >= size:
80 x = ""
81 else:
82 x = array[i]
83 texts.append(x)
84 while texts and not texts[-1]:
85 del texts[-1]
86 for col in range(len(texts)):
87 if ljust:
88 texts[col] = texts[col].ljust(colwidths[col])
89 else:
90 texts[col] = texts[col].rjust(colwidths[col])
91 pass
92 pass
93 s += "%s%s\n" % (lineprefix, str(colsep.join(texts)))
94 pass
95 return s
96 else:
97 array_index = lambda nrows, row, col: ncols*(row-1) + col
98 # Try every column count from size downwards
99 prev_colwidths = []
100 colwidths = []
101 for ncols in range(size, 0, -1):
102 # Try every row count from 1 upwards
103 min_rows = (size+ncols-1) // ncols
104 for nrows in range(min_rows, size):
105 rounded_size = nrows * ncols
106 colwidths = []
107 totwidth = -len(colsep)
108 for col in range(ncols):
109 # get max column width for this column
110 colwidth = 0
111 for row in range(1, nrows+1):
112 i = array_index(nrows, row, col)
113 if i >= rounded_size: break
114 elif i < size:
115 x = array[i]
116 colwidth = max(colwidth, len(x))
117 pass
118 pass
119 colwidths.append(colwidth)
120 totwidth += colwidth + len(colsep)
121 if totwidth >= displaywidth:
122 break
123 pass
124 if totwidth <= displaywidth and i >= rounded_size-1:
125 # Found the right nrows and ncols
126 nrows = row
127 break
128 elif totwidth >= displaywidth:
129 # Need to reduce ncols
130 break
131 pass
132 if totwidth <= displaywidth and i >= rounded_size-1:
133 break
134 pass
135 # The smallest number of rows computed and the
136 # max widths for each column has been obtained.
137 # Now we just have to format each of the
138 # rows.
139 s = ''
140 for row in range(1, nrows+1):
141 texts = []
142 for col in range(ncols):
143 i = array_index(nrows, row, col)
144 if i >= size:
145 break
146 else: x = array[i]
147 texts.append(x)
148 pass
149 for col in range(len(texts)):
150 if ljust:
151 texts[col] = texts[col].ljust(colwidths[col])
152 else:
153 texts[col] = texts[col].rjust(colwidths[col])
154 pass
155 pass
156 s += "%s%s\n" % (lineprefix, str(colsep.join(texts)))
157 pass
158 return s
159 pass
160
@@ -1,892 +1,931 b''
1 # Standard library imports
1 # Standard library imports
2 import sys
2 import sys
3
3
4 # System library imports
4 # System library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7 # Local imports
7 # Local imports
8 from IPython.external.columnize import columnize
8 from ansi_code_processor import QtAnsiCodeProcessor
9 from ansi_code_processor import QtAnsiCodeProcessor
9 from completion_widget import CompletionWidget
10 from completion_widget import CompletionWidget
10
11
11
12
12 class ConsoleWidget(QtGui.QPlainTextEdit):
13 class ConsoleWidget(QtGui.QPlainTextEdit):
13 """ 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
14 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
15 handling ANSI escape sequences.
16 handling ANSI escape sequences.
16 """
17 """
17
18
18 # Whether to process ANSI escape codes.
19 # Whether to process ANSI escape codes.
19 ansi_codes = True
20 ansi_codes = True
20
21
21 # The maximum number of lines of text before truncation.
22 # The maximum number of lines of text before truncation.
22 buffer_size = 500
23 buffer_size = 500
23
24
24 # 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.
25 gui_completion = True
26 gui_completion = True
26
27
27 # Whether to override ShortcutEvents for the keybindings defined by this
28 # Whether to override ShortcutEvents for the keybindings defined by this
28 # 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
29 # 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.
30 override_shortcuts = False
31 override_shortcuts = False
31
32
32 # The number of spaces to show for a tab character.
33 tab_width = 8
34
35 # Protected class variables.
33 # Protected class variables.
36 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
34 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
37 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
35 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
38 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
36 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
39 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
37 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
40 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
38 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
41 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
39 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
42 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
40 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
43 _shortcuts = set(_ctrl_down_remap.keys() +
41 _shortcuts = set(_ctrl_down_remap.keys() +
44 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
42 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
45
43
46 #---------------------------------------------------------------------------
44 #---------------------------------------------------------------------------
47 # 'QObject' interface
45 # 'QObject' interface
48 #---------------------------------------------------------------------------
46 #---------------------------------------------------------------------------
49
47
50 def __init__(self, parent=None):
48 def __init__(self, parent=None):
51 QtGui.QPlainTextEdit.__init__(self, parent)
49 QtGui.QPlainTextEdit.__init__(self, parent)
52
50
53 # Initialize protected variables. Some variables contain useful state
51 # Initialize protected variables. Some variables contain useful state
54 # information for subclasses; they should be considered read-only.
52 # information for subclasses; they should be considered read-only.
55 self._ansi_processor = QtAnsiCodeProcessor()
53 self._ansi_processor = QtAnsiCodeProcessor()
56 self._completion_widget = CompletionWidget(self)
54 self._completion_widget = CompletionWidget(self)
57 self._continuation_prompt = '> '
55 self._continuation_prompt = '> '
58 self._continuation_prompt_html = None
56 self._continuation_prompt_html = None
59 self._executing = False
57 self._executing = False
60 self._prompt = ''
58 self._prompt = ''
61 self._prompt_html = None
59 self._prompt_html = None
62 self._prompt_pos = 0
60 self._prompt_pos = 0
63 self._reading = False
61 self._reading = False
64 self._reading_callback = None
62 self._reading_callback = None
63 self._tab_width = 8
65
64
66 # Set a monospaced font.
65 # Set a monospaced font.
67 self.reset_font()
66 self.reset_font()
68
67
69 # Define a custom context menu.
68 # Define a custom context menu.
70 self._context_menu = QtGui.QMenu(self)
69 self._context_menu = QtGui.QMenu(self)
71
70
72 copy_action = QtGui.QAction('Copy', self)
71 copy_action = QtGui.QAction('Copy', self)
73 copy_action.triggered.connect(self.copy)
72 copy_action.triggered.connect(self.copy)
74 self.copyAvailable.connect(copy_action.setEnabled)
73 self.copyAvailable.connect(copy_action.setEnabled)
75 self._context_menu.addAction(copy_action)
74 self._context_menu.addAction(copy_action)
76
75
77 self._paste_action = QtGui.QAction('Paste', self)
76 self._paste_action = QtGui.QAction('Paste', self)
78 self._paste_action.triggered.connect(self.paste)
77 self._paste_action.triggered.connect(self.paste)
79 self._context_menu.addAction(self._paste_action)
78 self._context_menu.addAction(self._paste_action)
80 self._context_menu.addSeparator()
79 self._context_menu.addSeparator()
81
80
82 select_all_action = QtGui.QAction('Select All', self)
81 select_all_action = QtGui.QAction('Select All', self)
83 select_all_action.triggered.connect(self.selectAll)
82 select_all_action.triggered.connect(self.selectAll)
84 self._context_menu.addAction(select_all_action)
83 self._context_menu.addAction(select_all_action)
85
84
86 def event(self, event):
85 def event(self, event):
87 """ Reimplemented to override shortcuts, if necessary.
86 """ Reimplemented to override shortcuts, if necessary.
88 """
87 """
89 # On Mac OS, it is always unnecessary to override shortcuts, hence the
88 # On Mac OS, it is always unnecessary to override shortcuts, hence the
90 # check below. Users should just use the Control key instead of the
89 # check below. Users should just use the Control key instead of the
91 # Command key.
90 # Command key.
92 if self.override_shortcuts and \
91 if self.override_shortcuts and \
93 sys.platform != 'darwin' and \
92 sys.platform != 'darwin' and \
94 event.type() == QtCore.QEvent.ShortcutOverride and \
93 event.type() == QtCore.QEvent.ShortcutOverride and \
95 self._control_down(event.modifiers()) and \
94 self._control_down(event.modifiers()) and \
96 event.key() in self._shortcuts:
95 event.key() in self._shortcuts:
97 event.accept()
96 event.accept()
98 return True
97 return True
99 else:
98 else:
100 return QtGui.QPlainTextEdit.event(self, event)
99 return QtGui.QPlainTextEdit.event(self, event)
101
100
102 #---------------------------------------------------------------------------
101 #---------------------------------------------------------------------------
103 # 'QWidget' interface
102 # 'QWidget' interface
104 #---------------------------------------------------------------------------
103 #---------------------------------------------------------------------------
105
104
106 def contextMenuEvent(self, event):
105 def contextMenuEvent(self, event):
107 """ Reimplemented to create a menu without destructive actions like
106 """ Reimplemented to create a menu without destructive actions like
108 'Cut' and 'Delete'.
107 'Cut' and 'Delete'.
109 """
108 """
110 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
109 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
111 self._paste_action.setEnabled(not clipboard_empty)
110 self._paste_action.setEnabled(not clipboard_empty)
112
111
113 self._context_menu.exec_(event.globalPos())
112 self._context_menu.exec_(event.globalPos())
114
113
115 def dragMoveEvent(self, event):
114 def dragMoveEvent(self, event):
116 """ Reimplemented to disable dropping text.
115 """ Reimplemented to disable moving text by drag and drop.
117 """
116 """
118 event.ignore()
117 event.ignore()
119
118
120 def keyPressEvent(self, event):
119 def keyPressEvent(self, event):
121 """ Reimplemented to create a console-like interface.
120 """ Reimplemented to create a console-like interface.
122 """
121 """
123 intercepted = False
122 intercepted = False
124 cursor = self.textCursor()
123 cursor = self.textCursor()
125 position = cursor.position()
124 position = cursor.position()
126 key = event.key()
125 key = event.key()
127 ctrl_down = self._control_down(event.modifiers())
126 ctrl_down = self._control_down(event.modifiers())
128 alt_down = event.modifiers() & QtCore.Qt.AltModifier
127 alt_down = event.modifiers() & QtCore.Qt.AltModifier
129 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
128 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
130
129
131 # Even though we have reimplemented 'paste', the C++ level slot is still
130 # Even though we have reimplemented 'paste', the C++ level slot is still
132 # called by Qt. So we intercept the key press here.
131 # called by Qt. So we intercept the key press here.
133 if event.matches(QtGui.QKeySequence.Paste):
132 if event.matches(QtGui.QKeySequence.Paste):
134 self.paste()
133 self.paste()
135 intercepted = True
134 intercepted = True
136
135
137 elif ctrl_down:
136 elif ctrl_down:
138 if key in self._ctrl_down_remap:
137 if key in self._ctrl_down_remap:
139 ctrl_down = False
138 ctrl_down = False
140 key = self._ctrl_down_remap[key]
139 key = self._ctrl_down_remap[key]
141 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
140 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
142 QtCore.Qt.NoModifier)
141 QtCore.Qt.NoModifier)
143
142
144 elif key == QtCore.Qt.Key_K:
143 elif key == QtCore.Qt.Key_K:
145 if self._in_buffer(position):
144 if self._in_buffer(position):
146 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
145 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
147 QtGui.QTextCursor.KeepAnchor)
146 QtGui.QTextCursor.KeepAnchor)
148 cursor.removeSelectedText()
147 cursor.removeSelectedText()
149 intercepted = True
148 intercepted = True
150
149
151 elif key == QtCore.Qt.Key_X:
150 elif key == QtCore.Qt.Key_X:
152 intercepted = True
151 intercepted = True
153
152
154 elif key == QtCore.Qt.Key_Y:
153 elif key == QtCore.Qt.Key_Y:
155 self.paste()
154 self.paste()
156 intercepted = True
155 intercepted = True
157
156
158 elif alt_down:
157 elif alt_down:
159 if key == QtCore.Qt.Key_B:
158 if key == QtCore.Qt.Key_B:
160 self.setTextCursor(self._get_word_start_cursor(position))
159 self.setTextCursor(self._get_word_start_cursor(position))
161 intercepted = True
160 intercepted = True
162
161
163 elif key == QtCore.Qt.Key_F:
162 elif key == QtCore.Qt.Key_F:
164 self.setTextCursor(self._get_word_end_cursor(position))
163 self.setTextCursor(self._get_word_end_cursor(position))
165 intercepted = True
164 intercepted = True
166
165
167 elif key == QtCore.Qt.Key_Backspace:
166 elif key == QtCore.Qt.Key_Backspace:
168 cursor = self._get_word_start_cursor(position)
167 cursor = self._get_word_start_cursor(position)
169 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
168 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
170 cursor.removeSelectedText()
169 cursor.removeSelectedText()
171 intercepted = True
170 intercepted = True
172
171
173 elif key == QtCore.Qt.Key_D:
172 elif key == QtCore.Qt.Key_D:
174 cursor = self._get_word_end_cursor(position)
173 cursor = self._get_word_end_cursor(position)
175 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
174 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
176 cursor.removeSelectedText()
175 cursor.removeSelectedText()
177 intercepted = True
176 intercepted = True
178
177
179 if self._completion_widget.isVisible():
178 if self._completion_widget.isVisible():
180 self._completion_widget.keyPressEvent(event)
179 self._completion_widget.keyPressEvent(event)
181 intercepted = event.isAccepted()
180 intercepted = event.isAccepted()
182
181
183 else:
182 else:
184 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
183 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
185 if self._reading:
184 if self._reading:
186 self.appendPlainText('\n')
185 self.appendPlainText('\n')
187 self._reading = False
186 self._reading = False
188 if self._reading_callback:
187 if self._reading_callback:
189 self._reading_callback()
188 self._reading_callback()
190 elif not self._executing:
189 elif not self._executing:
191 self.execute(interactive=True)
190 self.execute(interactive=True)
192 intercepted = True
191 intercepted = True
193
192
194 elif key == QtCore.Qt.Key_Up:
193 elif key == QtCore.Qt.Key_Up:
195 if self._reading or not self._up_pressed():
194 if self._reading or not self._up_pressed():
196 intercepted = True
195 intercepted = True
197 else:
196 else:
198 prompt_line = self._get_prompt_cursor().blockNumber()
197 prompt_line = self._get_prompt_cursor().blockNumber()
199 intercepted = cursor.blockNumber() <= prompt_line
198 intercepted = cursor.blockNumber() <= prompt_line
200
199
201 elif key == QtCore.Qt.Key_Down:
200 elif key == QtCore.Qt.Key_Down:
202 if self._reading or not self._down_pressed():
201 if self._reading or not self._down_pressed():
203 intercepted = True
202 intercepted = True
204 else:
203 else:
205 end_line = self._get_end_cursor().blockNumber()
204 end_line = self._get_end_cursor().blockNumber()
206 intercepted = cursor.blockNumber() == end_line
205 intercepted = cursor.blockNumber() == end_line
207
206
208 elif key == QtCore.Qt.Key_Tab:
207 elif key == QtCore.Qt.Key_Tab:
209 if self._reading:
208 if self._reading:
210 intercepted = False
209 intercepted = False
211 else:
210 else:
212 intercepted = not self._tab_pressed()
211 intercepted = not self._tab_pressed()
213
212
214 elif key == QtCore.Qt.Key_Left:
213 elif key == QtCore.Qt.Key_Left:
215 intercepted = not self._in_buffer(position - 1)
214 intercepted = not self._in_buffer(position - 1)
216
215
217 elif key == QtCore.Qt.Key_Home:
216 elif key == QtCore.Qt.Key_Home:
218 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
217 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
219 start_pos = cursor.position()
218 start_pos = cursor.position()
220 start_line = cursor.blockNumber()
219 start_line = cursor.blockNumber()
221 if start_line == self._get_prompt_cursor().blockNumber():
220 if start_line == self._get_prompt_cursor().blockNumber():
222 start_pos += len(self._prompt)
221 start_pos += len(self._prompt)
223 else:
222 else:
224 start_pos += len(self._continuation_prompt)
223 start_pos += len(self._continuation_prompt)
225 if shift_down and self._in_buffer(position):
224 if shift_down and self._in_buffer(position):
226 self._set_selection(position, start_pos)
225 self._set_selection(position, start_pos)
227 else:
226 else:
228 self._set_position(start_pos)
227 self._set_position(start_pos)
229 intercepted = True
228 intercepted = True
230
229
231 elif key == QtCore.Qt.Key_Backspace and not alt_down:
230 elif key == QtCore.Qt.Key_Backspace and not alt_down:
232
231
233 # Line deletion (remove continuation prompt)
232 # Line deletion (remove continuation prompt)
234 len_prompt = len(self._continuation_prompt)
233 len_prompt = len(self._continuation_prompt)
235 if not self._reading and \
234 if not self._reading and \
236 cursor.columnNumber() == len_prompt and \
235 cursor.columnNumber() == len_prompt and \
237 position != self._prompt_pos:
236 position != self._prompt_pos:
238 cursor.setPosition(position - len_prompt,
237 cursor.setPosition(position - len_prompt,
239 QtGui.QTextCursor.KeepAnchor)
238 QtGui.QTextCursor.KeepAnchor)
240 cursor.removeSelectedText()
239 cursor.removeSelectedText()
241
240
242 # Regular backwards deletion
241 # Regular backwards deletion
243 else:
242 else:
244 anchor = cursor.anchor()
243 anchor = cursor.anchor()
245 if anchor == position:
244 if anchor == position:
246 intercepted = not self._in_buffer(position - 1)
245 intercepted = not self._in_buffer(position - 1)
247 else:
246 else:
248 intercepted = not self._in_buffer(min(anchor, position))
247 intercepted = not self._in_buffer(min(anchor, position))
249
248
250 elif key == QtCore.Qt.Key_Delete:
249 elif key == QtCore.Qt.Key_Delete:
251 anchor = cursor.anchor()
250 anchor = cursor.anchor()
252 intercepted = not self._in_buffer(min(anchor, position))
251 intercepted = not self._in_buffer(min(anchor, position))
253
252
254 # Don't move cursor if control is down to allow copy-paste using
253 # Don't move cursor if control is down to allow copy-paste using
255 # the keyboard in any part of the buffer.
254 # the keyboard in any part of the buffer.
256 if not ctrl_down:
255 if not ctrl_down:
257 self._keep_cursor_in_buffer()
256 self._keep_cursor_in_buffer()
258
257
259 if not intercepted:
258 if not intercepted:
260 QtGui.QPlainTextEdit.keyPressEvent(self, event)
259 QtGui.QPlainTextEdit.keyPressEvent(self, event)
261
260
262 #--------------------------------------------------------------------------
261 #--------------------------------------------------------------------------
263 # 'QPlainTextEdit' interface
262 # 'QPlainTextEdit' interface
264 #--------------------------------------------------------------------------
263 #--------------------------------------------------------------------------
265
264
266 def appendHtml(self, html):
265 def appendHtml(self, html):
267 """ Reimplemented to not append HTML as a new paragraph, which doesn't
266 """ Reimplemented to not append HTML as a new paragraph, which doesn't
268 make sense for a console widget.
267 make sense for a console widget.
269 """
268 """
270 cursor = self._get_end_cursor()
269 cursor = self._get_end_cursor()
271 cursor.insertHtml(html)
270 cursor.insertHtml(html)
272
271
273 # After appending HTML, the text document "remembers" the current
272 # After appending HTML, the text document "remembers" the current
274 # formatting, which means that subsequent calls to 'appendPlainText'
273 # formatting, which means that subsequent calls to 'appendPlainText'
275 # will be formatted similarly, a behavior that we do not want. To
274 # will be formatted similarly, a behavior that we do not want. To
276 # prevent this, we make sure that the last character has no formatting.
275 # prevent this, we make sure that the last character has no formatting.
277 cursor.movePosition(QtGui.QTextCursor.Left,
276 cursor.movePosition(QtGui.QTextCursor.Left,
278 QtGui.QTextCursor.KeepAnchor)
277 QtGui.QTextCursor.KeepAnchor)
279 if cursor.selection().toPlainText().trimmed().isEmpty():
278 if cursor.selection().toPlainText().trimmed().isEmpty():
280 # If the last character is whitespace, it doesn't matter how it's
279 # If the last character is whitespace, it doesn't matter how it's
281 # formatted, so just clear the formatting.
280 # formatted, so just clear the formatting.
282 cursor.setCharFormat(QtGui.QTextCharFormat())
281 cursor.setCharFormat(QtGui.QTextCharFormat())
283 else:
282 else:
284 # Otherwise, add an unformatted space.
283 # Otherwise, add an unformatted space.
285 cursor.movePosition(QtGui.QTextCursor.Right)
284 cursor.movePosition(QtGui.QTextCursor.Right)
286 cursor.insertText(' ', QtGui.QTextCharFormat())
285 cursor.insertText(' ', QtGui.QTextCharFormat())
287
286
288 def appendPlainText(self, text):
287 def appendPlainText(self, text):
289 """ Reimplemented to not append text as a new paragraph, which doesn't
288 """ Reimplemented to not append text as a new paragraph, which doesn't
290 make sense for a console widget. Also, if enabled, handle ANSI
289 make sense for a console widget. Also, if enabled, handle ANSI
291 codes.
290 codes.
292 """
291 """
293 cursor = self._get_end_cursor()
292 cursor = self._get_end_cursor()
294 if self.ansi_codes:
293 if self.ansi_codes:
295 for substring in self._ansi_processor.split_string(text):
294 for substring in self._ansi_processor.split_string(text):
296 format = self._ansi_processor.get_format()
295 format = self._ansi_processor.get_format()
297 cursor.insertText(substring, format)
296 cursor.insertText(substring, format)
298 else:
297 else:
299 cursor.insertText(text)
298 cursor.insertText(text)
300
299
301 def clear(self, keep_input=False):
300 def clear(self, keep_input=False):
302 """ Reimplemented to write a new prompt. If 'keep_input' is set,
301 """ Reimplemented to write a new prompt. If 'keep_input' is set,
303 restores the old input buffer when the new prompt is written.
302 restores the old input buffer when the new prompt is written.
304 """
303 """
305 QtGui.QPlainTextEdit.clear(self)
304 QtGui.QPlainTextEdit.clear(self)
306 if keep_input:
305 if keep_input:
307 input_buffer = self.input_buffer
306 input_buffer = self.input_buffer
308 self._show_prompt()
307 self._show_prompt()
309 if keep_input:
308 if keep_input:
310 self.input_buffer = input_buffer
309 self.input_buffer = input_buffer
311
310
312 def paste(self):
311 def paste(self):
313 """ Reimplemented to ensure that text is pasted in the editing region.
312 """ Reimplemented to ensure that text is pasted in the editing region.
314 """
313 """
315 self._keep_cursor_in_buffer()
314 self._keep_cursor_in_buffer()
316 QtGui.QPlainTextEdit.paste(self)
315 QtGui.QPlainTextEdit.paste(self)
317
316
318 def print_(self, printer):
317 def print_(self, printer):
319 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
318 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
320 slot has the wrong signature.
319 slot has the wrong signature.
321 """
320 """
322 QtGui.QPlainTextEdit.print_(self, printer)
321 QtGui.QPlainTextEdit.print_(self, printer)
323
322
324 #---------------------------------------------------------------------------
323 #---------------------------------------------------------------------------
325 # 'ConsoleWidget' public interface
324 # 'ConsoleWidget' public interface
326 #---------------------------------------------------------------------------
325 #---------------------------------------------------------------------------
327
326
328 def execute(self, source=None, hidden=False, interactive=False):
327 def execute(self, source=None, hidden=False, interactive=False):
329 """ Executes source or the input buffer, possibly prompting for more
328 """ Executes source or the input buffer, possibly prompting for more
330 input.
329 input.
331
330
332 Parameters:
331 Parameters:
333 -----------
332 -----------
334 source : str, optional
333 source : str, optional
335
334
336 The source to execute. If not specified, the input buffer will be
335 The source to execute. If not specified, the input buffer will be
337 used. If specified and 'hidden' is False, the input buffer will be
336 used. If specified and 'hidden' is False, the input buffer will be
338 replaced with the source before execution.
337 replaced with the source before execution.
339
338
340 hidden : bool, optional (default False)
339 hidden : bool, optional (default False)
341
340
342 If set, no output will be shown and the prompt will not be modified.
341 If set, no output will be shown and the prompt will not be modified.
343 In other words, it will be completely invisible to the user that
342 In other words, it will be completely invisible to the user that
344 an execution has occurred.
343 an execution has occurred.
345
344
346 interactive : bool, optional (default False)
345 interactive : bool, optional (default False)
347
346
348 Whether the console is to treat the source as having been manually
347 Whether the console is to treat the source as having been manually
349 entered by the user. The effect of this parameter depends on the
348 entered by the user. The effect of this parameter depends on the
350 subclass implementation.
349 subclass implementation.
351
350
352 Raises:
351 Raises:
353 -------
352 -------
354 RuntimeError
353 RuntimeError
355 If incomplete input is given and 'hidden' is True. In this case,
354 If incomplete input is given and 'hidden' is True. In this case,
356 it not possible to prompt for more input.
355 it not possible to prompt for more input.
357
356
358 Returns:
357 Returns:
359 --------
358 --------
360 A boolean indicating whether the source was executed.
359 A boolean indicating whether the source was executed.
361 """
360 """
362 if not hidden:
361 if not hidden:
363 if source is not None:
362 if source is not None:
364 self.input_buffer = source
363 self.input_buffer = source
365
364
366 self.appendPlainText('\n')
365 self.appendPlainText('\n')
367 self._executing_input_buffer = self.input_buffer
366 self._executing_input_buffer = self.input_buffer
368 self._executing = True
367 self._executing = True
369 self._prompt_finished()
368 self._prompt_finished()
370
369
371 real_source = self.input_buffer if source is None else source
370 real_source = self.input_buffer if source is None else source
372 complete = self._is_complete(real_source, interactive)
371 complete = self._is_complete(real_source, interactive)
373 if complete:
372 if complete:
374 if not hidden:
373 if not hidden:
375 # The maximum block count is only in effect during execution.
374 # The maximum block count is only in effect during execution.
376 # This ensures that _prompt_pos does not become invalid due to
375 # This ensures that _prompt_pos does not become invalid due to
377 # text truncation.
376 # text truncation.
378 self.setMaximumBlockCount(self.buffer_size)
377 self.setMaximumBlockCount(self.buffer_size)
379 self._execute(real_source, hidden)
378 self._execute(real_source, hidden)
380 elif hidden:
379 elif hidden:
381 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
380 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
382 else:
381 else:
383 self._show_continuation_prompt()
382 self._show_continuation_prompt()
384
383
385 return complete
384 return complete
386
385
387 def _get_input_buffer(self):
386 def _get_input_buffer(self):
388 """ The text that the user has entered entered at the current prompt.
387 """ The text that the user has entered entered at the current prompt.
389 """
388 """
390 # If we're executing, the input buffer may not even exist anymore due to
389 # If we're executing, the input buffer may not even exist anymore due to
391 # the limit imposed by 'buffer_size'. Therefore, we store it.
390 # the limit imposed by 'buffer_size'. Therefore, we store it.
392 if self._executing:
391 if self._executing:
393 return self._executing_input_buffer
392 return self._executing_input_buffer
394
393
395 cursor = self._get_end_cursor()
394 cursor = self._get_end_cursor()
396 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
395 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
397 input_buffer = str(cursor.selection().toPlainText())
396 input_buffer = str(cursor.selection().toPlainText())
398
397
399 # Strip out continuation prompts.
398 # Strip out continuation prompts.
400 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
399 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
401
400
402 def _set_input_buffer(self, string):
401 def _set_input_buffer(self, string):
403 """ Replaces the text in the input buffer with 'string'.
402 """ Replaces the text in the input buffer with 'string'.
404 """
403 """
405 # Remove old text.
404 # Remove old text.
406 cursor = self._get_end_cursor()
405 cursor = self._get_end_cursor()
407 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
406 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
408 cursor.removeSelectedText()
407 cursor.removeSelectedText()
409
408
410 # Insert new text with continuation prompts.
409 # Insert new text with continuation prompts.
411 lines = string.splitlines(True)
410 lines = string.splitlines(True)
412 if lines:
411 if lines:
413 self.appendPlainText(lines[0])
412 self.appendPlainText(lines[0])
414 for i in xrange(1, len(lines)):
413 for i in xrange(1, len(lines)):
415 if self._continuation_prompt_html is None:
414 if self._continuation_prompt_html is None:
416 self.appendPlainText(self._continuation_prompt)
415 self.appendPlainText(self._continuation_prompt)
417 else:
416 else:
418 self.appendHtml(self._continuation_prompt_html)
417 self.appendHtml(self._continuation_prompt_html)
419 self.appendPlainText(lines[i])
418 self.appendPlainText(lines[i])
420 self.moveCursor(QtGui.QTextCursor.End)
419 self.moveCursor(QtGui.QTextCursor.End)
421
420
422 input_buffer = property(_get_input_buffer, _set_input_buffer)
421 input_buffer = property(_get_input_buffer, _set_input_buffer)
423
422
424 def _get_input_buffer_cursor_line(self):
423 def _get_input_buffer_cursor_line(self):
425 """ The text in the line of the input buffer in which the user's cursor
424 """ The text in the line of the input buffer in which the user's cursor
426 rests. Returns a string if there is such a line; otherwise, None.
425 rests. Returns a string if there is such a line; otherwise, None.
427 """
426 """
428 if self._executing:
427 if self._executing:
429 return None
428 return None
430 cursor = self.textCursor()
429 cursor = self.textCursor()
431 if cursor.position() >= self._prompt_pos:
430 if cursor.position() >= self._prompt_pos:
432 text = self._get_block_plain_text(cursor.block())
431 text = self._get_block_plain_text(cursor.block())
433 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
432 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
434 return text[len(self._prompt):]
433 return text[len(self._prompt):]
435 else:
434 else:
436 return text[len(self._continuation_prompt):]
435 return text[len(self._continuation_prompt):]
437 else:
436 else:
438 return None
437 return None
439
438
440 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
439 input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
441
440
442 def _get_font(self):
441 def _get_font(self):
443 """ The base font being used by the ConsoleWidget.
442 """ The base font being used by the ConsoleWidget.
444 """
443 """
445 return self.document().defaultFont()
444 return self.document().defaultFont()
446
445
447 def _set_font(self, font):
446 def _set_font(self, font):
448 """ Sets the base font for the ConsoleWidget to the specified QFont.
447 """ Sets the base font for the ConsoleWidget to the specified QFont.
449 """
448 """
450 font_metrics = QtGui.QFontMetrics(font)
449 font_metrics = QtGui.QFontMetrics(font)
451 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
450 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
452
451
453 self._completion_widget.setFont(font)
452 self._completion_widget.setFont(font)
454 self.document().setDefaultFont(font)
453 self.document().setDefaultFont(font)
455
454
456 font = property(_get_font, _set_font)
455 font = property(_get_font, _set_font)
457
456
458 def reset_font(self):
457 def reset_font(self):
459 """ Sets the font to the default fixed-width font for this platform.
458 """ Sets the font to the default fixed-width font for this platform.
460 """
459 """
461 if sys.platform == 'win32':
460 if sys.platform == 'win32':
462 name = 'Courier'
461 name = 'Courier'
463 elif sys.platform == 'darwin':
462 elif sys.platform == 'darwin':
464 name = 'Monaco'
463 name = 'Monaco'
465 else:
464 else:
466 name = 'Monospace'
465 name = 'Monospace'
467 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
466 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
468 font.setStyleHint(QtGui.QFont.TypeWriter)
467 font.setStyleHint(QtGui.QFont.TypeWriter)
469 self._set_font(font)
468 self._set_font(font)
469
470 def _get_tab_width(self):
471 """ The width (in terms of space characters) for tab characters.
472 """
473 return self._tab_width
474
475 def _set_tab_width(self, tab_width):
476 """ Sets the width (in terms of space characters) for tab characters.
477 """
478 font_metrics = QtGui.QFontMetrics(self.font)
479 self.setTabStopWidth(tab_width * font_metrics.width(' '))
480
481 self._tab_width = tab_width
482
483 tab_width = property(_get_tab_width, _set_tab_width)
470
484
471 #---------------------------------------------------------------------------
485 #---------------------------------------------------------------------------
472 # 'ConsoleWidget' abstract interface
486 # 'ConsoleWidget' abstract interface
473 #---------------------------------------------------------------------------
487 #---------------------------------------------------------------------------
474
488
475 def _is_complete(self, source, interactive):
489 def _is_complete(self, source, interactive):
476 """ Returns whether 'source' can be executed. When triggered by an
490 """ Returns whether 'source' can be executed. When triggered by an
477 Enter/Return key press, 'interactive' is True; otherwise, it is
491 Enter/Return key press, 'interactive' is True; otherwise, it is
478 False.
492 False.
479 """
493 """
480 raise NotImplementedError
494 raise NotImplementedError
481
495
482 def _execute(self, source, hidden):
496 def _execute(self, source, hidden):
483 """ Execute 'source'. If 'hidden', do not show any output.
497 """ Execute 'source'. If 'hidden', do not show any output.
484 """
498 """
485 raise NotImplementedError
499 raise NotImplementedError
486
500
487 def _prompt_started_hook(self):
501 def _prompt_started_hook(self):
488 """ Called immediately after a new prompt is displayed.
502 """ Called immediately after a new prompt is displayed.
489 """
503 """
490 pass
504 pass
491
505
492 def _prompt_finished_hook(self):
506 def _prompt_finished_hook(self):
493 """ Called immediately after a prompt is finished, i.e. when some input
507 """ Called immediately after a prompt is finished, i.e. when some input
494 will be processed and a new prompt displayed.
508 will be processed and a new prompt displayed.
495 """
509 """
496 pass
510 pass
497
511
498 def _up_pressed(self):
512 def _up_pressed(self):
499 """ Called when the up key is pressed. Returns whether to continue
513 """ Called when the up key is pressed. Returns whether to continue
500 processing the event.
514 processing the event.
501 """
515 """
502 return True
516 return True
503
517
504 def _down_pressed(self):
518 def _down_pressed(self):
505 """ Called when the down key is pressed. Returns whether to continue
519 """ Called when the down key is pressed. Returns whether to continue
506 processing the event.
520 processing the event.
507 """
521 """
508 return True
522 return True
509
523
510 def _tab_pressed(self):
524 def _tab_pressed(self):
511 """ Called when the tab key is pressed. Returns whether to continue
525 """ Called when the tab key is pressed. Returns whether to continue
512 processing the event.
526 processing the event.
513 """
527 """
514 return False
528 return False
515
529
516 #--------------------------------------------------------------------------
530 #--------------------------------------------------------------------------
517 # 'ConsoleWidget' protected interface
531 # 'ConsoleWidget' protected interface
518 #--------------------------------------------------------------------------
532 #--------------------------------------------------------------------------
519
533
520 def _append_html_fetching_plain_text(self, html):
534 def _append_html_fetching_plain_text(self, html):
521 """ Appends 'html', then returns the plain text version of it.
535 """ Appends 'html', then returns the plain text version of it.
522 """
536 """
523 anchor = self._get_end_cursor().position()
537 anchor = self._get_end_cursor().position()
524 self.appendHtml(html)
538 self.appendHtml(html)
525 cursor = self._get_end_cursor()
539 cursor = self._get_end_cursor()
526 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
540 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
527 return str(cursor.selection().toPlainText())
541 return str(cursor.selection().toPlainText())
528
542
529 def _append_plain_text_keeping_prompt(self, text):
543 def _append_plain_text_keeping_prompt(self, text):
530 """ Writes 'text' after the current prompt, then restores the old prompt
544 """ Writes 'text' after the current prompt, then restores the old prompt
531 with its old input buffer.
545 with its old input buffer.
532 """
546 """
533 input_buffer = self.input_buffer
547 input_buffer = self.input_buffer
534 self.appendPlainText('\n')
548 self.appendPlainText('\n')
535 self._prompt_finished()
549 self._prompt_finished()
536
550
537 self.appendPlainText(text)
551 self.appendPlainText(text)
538 self._show_prompt()
552 self._show_prompt()
539 self.input_buffer = input_buffer
553 self.input_buffer = input_buffer
540
554
541 def _control_down(self, modifiers):
555 def _control_down(self, modifiers):
542 """ Given a KeyboardModifiers flags object, return whether the Control
556 """ Given a KeyboardModifiers flags object, return whether the Control
543 key is down (on Mac OS, treat the Command key as a synonym for
557 key is down (on Mac OS, treat the Command key as a synonym for
544 Control).
558 Control).
545 """
559 """
546 down = bool(modifiers & QtCore.Qt.ControlModifier)
560 down = bool(modifiers & QtCore.Qt.ControlModifier)
547
561
548 # Note: on Mac OS, ControlModifier corresponds to the Command key while
562 # Note: on Mac OS, ControlModifier corresponds to the Command key while
549 # MetaModifier corresponds to the Control key.
563 # MetaModifier corresponds to the Control key.
550 if sys.platform == 'darwin':
564 if sys.platform == 'darwin':
551 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
565 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
552
566
553 return down
567 return down
554
568
555 def _complete_with_items(self, cursor, items):
569 def _complete_with_items(self, cursor, items):
556 """ Performs completion with 'items' at the specified cursor location.
570 """ Performs completion with 'items' at the specified cursor location.
557 """
571 """
558 if len(items) == 1:
572 if len(items) == 1:
559 cursor.setPosition(self.textCursor().position(),
573 cursor.setPosition(self.textCursor().position(),
560 QtGui.QTextCursor.KeepAnchor)
574 QtGui.QTextCursor.KeepAnchor)
561 cursor.insertText(items[0])
575 cursor.insertText(items[0])
562 elif len(items) > 1:
576 elif len(items) > 1:
563 if self.gui_completion:
577 if self.gui_completion:
564 self._completion_widget.show_items(cursor, items)
578 self._completion_widget.show_items(cursor, items)
565 else:
579 else:
566 text = '\n'.join(items) + '\n'
580 text = self.format_as_columns(items)
567 self._append_plain_text_keeping_prompt(text)
581 self._append_plain_text_keeping_prompt(text)
568
582
583 def format_as_columns(self, items, separator=' ', vertical=True):
584 """ Transform a list of strings into a single string with columns.
585
586 Parameters
587 ----------
588 items : sequence [str]
589 The strings to process.
590
591 separator : str, optional [default is two spaces]
592 The string that separates columns.
593
594 vertical: bool, optional [default True]
595 If set, consecutive items will be arranged from top to bottom, then
596 from left to right. Otherwise, consecutive items will be aranged
597 from left to right, then from top to bottom.
598
599 Returns
600 -------
601 The formatted string.
602 """
603 font_metrics = QtGui.QFontMetrics(self.font)
604 width = self.width() / font_metrics.width(' ')
605 return columnize(items, displaywidth=width,
606 colsep=separator, arrange_vertical=vertical)
607
569 def _get_block_plain_text(self, block):
608 def _get_block_plain_text(self, block):
570 """ Given a QTextBlock, return its unformatted text.
609 """ Given a QTextBlock, return its unformatted text.
571 """
610 """
572 cursor = QtGui.QTextCursor(block)
611 cursor = QtGui.QTextCursor(block)
573 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
612 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
574 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
613 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
575 QtGui.QTextCursor.KeepAnchor)
614 QtGui.QTextCursor.KeepAnchor)
576 return str(cursor.selection().toPlainText())
615 return str(cursor.selection().toPlainText())
577
616
578 def _get_end_cursor(self):
617 def _get_end_cursor(self):
579 """ Convenience method that returns a cursor for the last character.
618 """ Convenience method that returns a cursor for the last character.
580 """
619 """
581 cursor = self.textCursor()
620 cursor = self.textCursor()
582 cursor.movePosition(QtGui.QTextCursor.End)
621 cursor.movePosition(QtGui.QTextCursor.End)
583 return cursor
622 return cursor
584
623
585 def _get_prompt_cursor(self):
624 def _get_prompt_cursor(self):
586 """ Convenience method that returns a cursor for the prompt position.
625 """ Convenience method that returns a cursor for the prompt position.
587 """
626 """
588 cursor = self.textCursor()
627 cursor = self.textCursor()
589 cursor.setPosition(self._prompt_pos)
628 cursor.setPosition(self._prompt_pos)
590 return cursor
629 return cursor
591
630
592 def _get_selection_cursor(self, start, end):
631 def _get_selection_cursor(self, start, end):
593 """ Convenience method that returns a cursor with text selected between
632 """ Convenience method that returns a cursor with text selected between
594 the positions 'start' and 'end'.
633 the positions 'start' and 'end'.
595 """
634 """
596 cursor = self.textCursor()
635 cursor = self.textCursor()
597 cursor.setPosition(start)
636 cursor.setPosition(start)
598 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
637 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
599 return cursor
638 return cursor
600
639
601 def _get_word_start_cursor(self, position):
640 def _get_word_start_cursor(self, position):
602 """ Find the start of the word to the left the given position. If a
641 """ Find the start of the word to the left the given position. If a
603 sequence of non-word characters precedes the first word, skip over
642 sequence of non-word characters precedes the first word, skip over
604 them. (This emulates the behavior of bash, emacs, etc.)
643 them. (This emulates the behavior of bash, emacs, etc.)
605 """
644 """
606 document = self.document()
645 document = self.document()
607 position -= 1
646 position -= 1
608 while self._in_buffer(position) and \
647 while self._in_buffer(position) and \
609 not document.characterAt(position).isLetterOrNumber():
648 not document.characterAt(position).isLetterOrNumber():
610 position -= 1
649 position -= 1
611 while self._in_buffer(position) and \
650 while self._in_buffer(position) and \
612 document.characterAt(position).isLetterOrNumber():
651 document.characterAt(position).isLetterOrNumber():
613 position -= 1
652 position -= 1
614 cursor = self.textCursor()
653 cursor = self.textCursor()
615 cursor.setPosition(position + 1)
654 cursor.setPosition(position + 1)
616 return cursor
655 return cursor
617
656
618 def _get_word_end_cursor(self, position):
657 def _get_word_end_cursor(self, position):
619 """ Find the end of the word to the right the given position. If a
658 """ Find the end of the word to the right the given position. If a
620 sequence of non-word characters precedes the first word, skip over
659 sequence of non-word characters precedes the first word, skip over
621 them. (This emulates the behavior of bash, emacs, etc.)
660 them. (This emulates the behavior of bash, emacs, etc.)
622 """
661 """
623 document = self.document()
662 document = self.document()
624 end = self._get_end_cursor().position()
663 end = self._get_end_cursor().position()
625 while position < end and \
664 while position < end and \
626 not document.characterAt(position).isLetterOrNumber():
665 not document.characterAt(position).isLetterOrNumber():
627 position += 1
666 position += 1
628 while position < end and \
667 while position < end and \
629 document.characterAt(position).isLetterOrNumber():
668 document.characterAt(position).isLetterOrNumber():
630 position += 1
669 position += 1
631 cursor = self.textCursor()
670 cursor = self.textCursor()
632 cursor.setPosition(position)
671 cursor.setPosition(position)
633 return cursor
672 return cursor
634
673
635 def _prompt_started(self):
674 def _prompt_started(self):
636 """ Called immediately after a new prompt is displayed.
675 """ Called immediately after a new prompt is displayed.
637 """
676 """
638 # Temporarily disable the maximum block count to permit undo/redo and
677 # Temporarily disable the maximum block count to permit undo/redo and
639 # to ensure that the prompt position does not change due to truncation.
678 # to ensure that the prompt position does not change due to truncation.
640 self.setMaximumBlockCount(0)
679 self.setMaximumBlockCount(0)
641 self.setUndoRedoEnabled(True)
680 self.setUndoRedoEnabled(True)
642
681
643 self.setReadOnly(False)
682 self.setReadOnly(False)
644 self.moveCursor(QtGui.QTextCursor.End)
683 self.moveCursor(QtGui.QTextCursor.End)
645 self.centerCursor()
684 self.centerCursor()
646
685
647 self._executing = False
686 self._executing = False
648 self._prompt_started_hook()
687 self._prompt_started_hook()
649
688
650 def _prompt_finished(self):
689 def _prompt_finished(self):
651 """ Called immediately after a prompt is finished, i.e. when some input
690 """ Called immediately after a prompt is finished, i.e. when some input
652 will be processed and a new prompt displayed.
691 will be processed and a new prompt displayed.
653 """
692 """
654 self.setUndoRedoEnabled(False)
693 self.setUndoRedoEnabled(False)
655 self.setReadOnly(True)
694 self.setReadOnly(True)
656 self._prompt_finished_hook()
695 self._prompt_finished_hook()
657
696
658 def _readline(self, prompt='', callback=None):
697 def _readline(self, prompt='', callback=None):
659 """ Reads one line of input from the user.
698 """ Reads one line of input from the user.
660
699
661 Parameters
700 Parameters
662 ----------
701 ----------
663 prompt : str, optional
702 prompt : str, optional
664 The prompt to print before reading the line.
703 The prompt to print before reading the line.
665
704
666 callback : callable, optional
705 callback : callable, optional
667 A callback to execute with the read line. If not specified, input is
706 A callback to execute with the read line. If not specified, input is
668 read *synchronously* and this method does not return until it has
707 read *synchronously* and this method does not return until it has
669 been read.
708 been read.
670
709
671 Returns
710 Returns
672 -------
711 -------
673 If a callback is specified, returns nothing. Otherwise, returns the
712 If a callback is specified, returns nothing. Otherwise, returns the
674 input string with the trailing newline stripped.
713 input string with the trailing newline stripped.
675 """
714 """
676 if self._reading:
715 if self._reading:
677 raise RuntimeError('Cannot read a line. Widget is already reading.')
716 raise RuntimeError('Cannot read a line. Widget is already reading.')
678
717
679 if not callback and not self.isVisible():
718 if not callback and not self.isVisible():
680 # If the user cannot see the widget, this function cannot return.
719 # If the user cannot see the widget, this function cannot return.
681 raise RuntimeError('Cannot synchronously read a line if the widget'
720 raise RuntimeError('Cannot synchronously read a line if the widget'
682 'is not visible!')
721 'is not visible!')
683
722
684 self._reading = True
723 self._reading = True
685 self._show_prompt(prompt, newline=False)
724 self._show_prompt(prompt, newline=False)
686
725
687 if callback is None:
726 if callback is None:
688 self._reading_callback = None
727 self._reading_callback = None
689 while self._reading:
728 while self._reading:
690 QtCore.QCoreApplication.processEvents()
729 QtCore.QCoreApplication.processEvents()
691 return self.input_buffer.rstrip('\n')
730 return self.input_buffer.rstrip('\n')
692
731
693 else:
732 else:
694 self._reading_callback = lambda: \
733 self._reading_callback = lambda: \
695 callback(self.input_buffer.rstrip('\n'))
734 callback(self.input_buffer.rstrip('\n'))
696
735
697 def _reset(self):
736 def _reset(self):
698 """ Clears the console and resets internal state variables.
737 """ Clears the console and resets internal state variables.
699 """
738 """
700 QtGui.QPlainTextEdit.clear(self)
739 QtGui.QPlainTextEdit.clear(self)
701 self._executing = self._reading = False
740 self._executing = self._reading = False
702
741
703 def _set_continuation_prompt(self, prompt, html=False):
742 def _set_continuation_prompt(self, prompt, html=False):
704 """ Sets the continuation prompt.
743 """ Sets the continuation prompt.
705
744
706 Parameters
745 Parameters
707 ----------
746 ----------
708 prompt : str
747 prompt : str
709 The prompt to show when more input is needed.
748 The prompt to show when more input is needed.
710
749
711 html : bool, optional (default False)
750 html : bool, optional (default False)
712 If set, the prompt will be inserted as formatted HTML. Otherwise,
751 If set, the prompt will be inserted as formatted HTML. Otherwise,
713 the prompt will be treated as plain text, though ANSI color codes
752 the prompt will be treated as plain text, though ANSI color codes
714 will be handled.
753 will be handled.
715 """
754 """
716 if html:
755 if html:
717 self._continuation_prompt_html = prompt
756 self._continuation_prompt_html = prompt
718 else:
757 else:
719 self._continuation_prompt = prompt
758 self._continuation_prompt = prompt
720 self._continuation_prompt_html = None
759 self._continuation_prompt_html = None
721
760
722 def _set_position(self, position):
761 def _set_position(self, position):
723 """ Convenience method to set the position of the cursor.
762 """ Convenience method to set the position of the cursor.
724 """
763 """
725 cursor = self.textCursor()
764 cursor = self.textCursor()
726 cursor.setPosition(position)
765 cursor.setPosition(position)
727 self.setTextCursor(cursor)
766 self.setTextCursor(cursor)
728
767
729 def _set_selection(self, start, end):
768 def _set_selection(self, start, end):
730 """ Convenience method to set the current selected text.
769 """ Convenience method to set the current selected text.
731 """
770 """
732 self.setTextCursor(self._get_selection_cursor(start, end))
771 self.setTextCursor(self._get_selection_cursor(start, end))
733
772
734 def _show_prompt(self, prompt=None, html=False, newline=True):
773 def _show_prompt(self, prompt=None, html=False, newline=True):
735 """ Writes a new prompt at the end of the buffer.
774 """ Writes a new prompt at the end of the buffer.
736
775
737 Parameters
776 Parameters
738 ----------
777 ----------
739 prompt : str, optional
778 prompt : str, optional
740 The prompt to show. If not specified, the previous prompt is used.
779 The prompt to show. If not specified, the previous prompt is used.
741
780
742 html : bool, optional (default False)
781 html : bool, optional (default False)
743 Only relevant when a prompt is specified. If set, the prompt will
782 Only relevant when a prompt is specified. If set, the prompt will
744 be inserted as formatted HTML. Otherwise, the prompt will be treated
783 be inserted as formatted HTML. Otherwise, the prompt will be treated
745 as plain text, though ANSI color codes will be handled.
784 as plain text, though ANSI color codes will be handled.
746
785
747 newline : bool, optional (default True)
786 newline : bool, optional (default True)
748 If set, a new line will be written before showing the prompt if
787 If set, a new line will be written before showing the prompt if
749 there is not already a newline at the end of the buffer.
788 there is not already a newline at the end of the buffer.
750 """
789 """
751 # Insert a preliminary newline, if necessary.
790 # Insert a preliminary newline, if necessary.
752 if newline:
791 if newline:
753 cursor = self._get_end_cursor()
792 cursor = self._get_end_cursor()
754 if cursor.position() > 0:
793 if cursor.position() > 0:
755 cursor.movePosition(QtGui.QTextCursor.Left,
794 cursor.movePosition(QtGui.QTextCursor.Left,
756 QtGui.QTextCursor.KeepAnchor)
795 QtGui.QTextCursor.KeepAnchor)
757 if str(cursor.selection().toPlainText()) != '\n':
796 if str(cursor.selection().toPlainText()) != '\n':
758 self.appendPlainText('\n')
797 self.appendPlainText('\n')
759
798
760 # Write the prompt.
799 # Write the prompt.
761 if prompt is None:
800 if prompt is None:
762 if self._prompt_html is None:
801 if self._prompt_html is None:
763 self.appendPlainText(self._prompt)
802 self.appendPlainText(self._prompt)
764 else:
803 else:
765 self.appendHtml(self._prompt_html)
804 self.appendHtml(self._prompt_html)
766 else:
805 else:
767 if html:
806 if html:
768 self._prompt = self._append_html_fetching_plain_text(prompt)
807 self._prompt = self._append_html_fetching_plain_text(prompt)
769 self._prompt_html = prompt
808 self._prompt_html = prompt
770 else:
809 else:
771 self.appendPlainText(prompt)
810 self.appendPlainText(prompt)
772 self._prompt = prompt
811 self._prompt = prompt
773 self._prompt_html = None
812 self._prompt_html = None
774
813
775 self._prompt_pos = self._get_end_cursor().position()
814 self._prompt_pos = self._get_end_cursor().position()
776 self._prompt_started()
815 self._prompt_started()
777
816
778 def _show_continuation_prompt(self):
817 def _show_continuation_prompt(self):
779 """ Writes a new continuation prompt at the end of the buffer.
818 """ Writes a new continuation prompt at the end of the buffer.
780 """
819 """
781 if self._continuation_prompt_html is None:
820 if self._continuation_prompt_html is None:
782 self.appendPlainText(self._continuation_prompt)
821 self.appendPlainText(self._continuation_prompt)
783 else:
822 else:
784 self._continuation_prompt = self._append_html_fetching_plain_text(
823 self._continuation_prompt = self._append_html_fetching_plain_text(
785 self._continuation_prompt_html)
824 self._continuation_prompt_html)
786
825
787 self._prompt_started()
826 self._prompt_started()
788
827
789 def _in_buffer(self, position):
828 def _in_buffer(self, position):
790 """ Returns whether the given position is inside the editing region.
829 """ Returns whether the given position is inside the editing region.
791 """
830 """
792 return position >= self._prompt_pos
831 return position >= self._prompt_pos
793
832
794 def _keep_cursor_in_buffer(self):
833 def _keep_cursor_in_buffer(self):
795 """ Ensures that the cursor is inside the editing region. Returns
834 """ Ensures that the cursor is inside the editing region. Returns
796 whether the cursor was moved.
835 whether the cursor was moved.
797 """
836 """
798 cursor = self.textCursor()
837 cursor = self.textCursor()
799 if cursor.position() < self._prompt_pos:
838 if cursor.position() < self._prompt_pos:
800 cursor.movePosition(QtGui.QTextCursor.End)
839 cursor.movePosition(QtGui.QTextCursor.End)
801 self.setTextCursor(cursor)
840 self.setTextCursor(cursor)
802 return True
841 return True
803 else:
842 else:
804 return False
843 return False
805
844
806
845
807 class HistoryConsoleWidget(ConsoleWidget):
846 class HistoryConsoleWidget(ConsoleWidget):
808 """ A ConsoleWidget that keeps a history of the commands that have been
847 """ A ConsoleWidget that keeps a history of the commands that have been
809 executed.
848 executed.
810 """
849 """
811
850
812 #---------------------------------------------------------------------------
851 #---------------------------------------------------------------------------
813 # 'QObject' interface
852 # 'QObject' interface
814 #---------------------------------------------------------------------------
853 #---------------------------------------------------------------------------
815
854
816 def __init__(self, parent=None):
855 def __init__(self, parent=None):
817 super(HistoryConsoleWidget, self).__init__(parent)
856 super(HistoryConsoleWidget, self).__init__(parent)
818
857
819 self._history = []
858 self._history = []
820 self._history_index = 0
859 self._history_index = 0
821
860
822 #---------------------------------------------------------------------------
861 #---------------------------------------------------------------------------
823 # 'ConsoleWidget' public interface
862 # 'ConsoleWidget' public interface
824 #---------------------------------------------------------------------------
863 #---------------------------------------------------------------------------
825
864
826 def execute(self, source=None, hidden=False, interactive=False):
865 def execute(self, source=None, hidden=False, interactive=False):
827 """ Reimplemented to the store history.
866 """ Reimplemented to the store history.
828 """
867 """
829 if not hidden:
868 if not hidden:
830 history = self.input_buffer if source is None else source
869 history = self.input_buffer if source is None else source
831
870
832 executed = super(HistoryConsoleWidget, self).execute(
871 executed = super(HistoryConsoleWidget, self).execute(
833 source, hidden, interactive)
872 source, hidden, interactive)
834
873
835 if executed and not hidden:
874 if executed and not hidden:
836 self._history.append(history.rstrip())
875 self._history.append(history.rstrip())
837 self._history_index = len(self._history)
876 self._history_index = len(self._history)
838
877
839 return executed
878 return executed
840
879
841 #---------------------------------------------------------------------------
880 #---------------------------------------------------------------------------
842 # 'ConsoleWidget' abstract interface
881 # 'ConsoleWidget' abstract interface
843 #---------------------------------------------------------------------------
882 #---------------------------------------------------------------------------
844
883
845 def _up_pressed(self):
884 def _up_pressed(self):
846 """ Called when the up key is pressed. Returns whether to continue
885 """ Called when the up key is pressed. Returns whether to continue
847 processing the event.
886 processing the event.
848 """
887 """
849 prompt_cursor = self._get_prompt_cursor()
888 prompt_cursor = self._get_prompt_cursor()
850 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
889 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
851 self.history_previous()
890 self.history_previous()
852
891
853 # Go to the first line of prompt for seemless history scrolling.
892 # Go to the first line of prompt for seemless history scrolling.
854 cursor = self._get_prompt_cursor()
893 cursor = self._get_prompt_cursor()
855 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
894 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
856 self.setTextCursor(cursor)
895 self.setTextCursor(cursor)
857
896
858 return False
897 return False
859 return True
898 return True
860
899
861 def _down_pressed(self):
900 def _down_pressed(self):
862 """ Called when the down key is pressed. Returns whether to continue
901 """ Called when the down key is pressed. Returns whether to continue
863 processing the event.
902 processing the event.
864 """
903 """
865 end_cursor = self._get_end_cursor()
904 end_cursor = self._get_end_cursor()
866 if self.textCursor().blockNumber() == end_cursor.blockNumber():
905 if self.textCursor().blockNumber() == end_cursor.blockNumber():
867 self.history_next()
906 self.history_next()
868 return False
907 return False
869 return True
908 return True
870
909
871 #---------------------------------------------------------------------------
910 #---------------------------------------------------------------------------
872 # 'HistoryConsoleWidget' interface
911 # 'HistoryConsoleWidget' interface
873 #---------------------------------------------------------------------------
912 #---------------------------------------------------------------------------
874
913
875 def history_previous(self):
914 def history_previous(self):
876 """ If possible, set the input buffer to the previous item in the
915 """ If possible, set the input buffer to the previous item in the
877 history.
916 history.
878 """
917 """
879 if self._history_index > 0:
918 if self._history_index > 0:
880 self._history_index -= 1
919 self._history_index -= 1
881 self.input_buffer = self._history[self._history_index]
920 self.input_buffer = self._history[self._history_index]
882
921
883 def history_next(self):
922 def history_next(self):
884 """ Set the input buffer to the next item in the history, or a blank
923 """ Set the input buffer to the next item in the history, or a blank
885 line if there is no subsequent item.
924 line if there is no subsequent item.
886 """
925 """
887 if self._history_index < len(self._history):
926 if self._history_index < len(self._history):
888 self._history_index += 1
927 self._history_index += 1
889 if self._history_index < len(self._history):
928 if self._history_index < len(self._history):
890 self.input_buffer = self._history[self._history_index]
929 self.input_buffer = self._history[self._history_index]
891 else:
930 else:
892 self.input_buffer = ''
931 self.input_buffer = ''
@@ -1,384 +1,382 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 call_tip_widget import CallTipWidget
12 from call_tip_widget import CallTipWidget
13 from completion_lexer import CompletionLexer
13 from completion_lexer import CompletionLexer
14 from console_widget import HistoryConsoleWidget
14 from console_widget import HistoryConsoleWidget
15 from pygments_highlighter import PygmentsHighlighter
15 from pygments_highlighter import PygmentsHighlighter
16
16
17
17
18 class FrontendHighlighter(PygmentsHighlighter):
18 class FrontendHighlighter(PygmentsHighlighter):
19 """ A PygmentsHighlighter that can be turned on and off and that ignores
19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 prompts.
20 prompts.
21 """
21 """
22
22
23 def __init__(self, frontend):
23 def __init__(self, frontend):
24 super(FrontendHighlighter, self).__init__(frontend.document())
24 super(FrontendHighlighter, self).__init__(frontend.document())
25 self._current_offset = 0
25 self._current_offset = 0
26 self._frontend = frontend
26 self._frontend = frontend
27 self.highlighting_on = False
27 self.highlighting_on = False
28
28
29 def highlightBlock(self, qstring):
29 def highlightBlock(self, qstring):
30 """ Highlight a block of text. Reimplemented to highlight selectively.
30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 """
31 """
32 if not self.highlighting_on:
32 if not self.highlighting_on:
33 return
33 return
34
34
35 # The input to this function is unicode string that may contain
35 # The input to this function is unicode string that may contain
36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 # the string as plain text so we can compare it.
37 # the string as plain text so we can compare it.
38 current_block = self.currentBlock()
38 current_block = self.currentBlock()
39 string = self._frontend._get_block_plain_text(current_block)
39 string = self._frontend._get_block_plain_text(current_block)
40
40
41 # Decide whether to check for the regular or continuation prompt.
41 # Decide whether to check for the regular or continuation prompt.
42 if current_block.contains(self._frontend._prompt_pos):
42 if current_block.contains(self._frontend._prompt_pos):
43 prompt = self._frontend._prompt
43 prompt = self._frontend._prompt
44 else:
44 else:
45 prompt = self._frontend._continuation_prompt
45 prompt = self._frontend._continuation_prompt
46
46
47 # Don't highlight the part of the string that contains the prompt.
47 # Don't highlight the part of the string that contains the prompt.
48 if string.startswith(prompt):
48 if string.startswith(prompt):
49 self._current_offset = len(prompt)
49 self._current_offset = len(prompt)
50 qstring.remove(0, len(prompt))
50 qstring.remove(0, len(prompt))
51 else:
51 else:
52 self._current_offset = 0
52 self._current_offset = 0
53
53
54 PygmentsHighlighter.highlightBlock(self, qstring)
54 PygmentsHighlighter.highlightBlock(self, qstring)
55
55
56 def setFormat(self, start, count, format):
56 def setFormat(self, start, count, format):
57 """ Reimplemented to highlight selectively.
57 """ Reimplemented to highlight selectively.
58 """
58 """
59 start += self._current_offset
59 start += self._current_offset
60 PygmentsHighlighter.setFormat(self, start, count, format)
60 PygmentsHighlighter.setFormat(self, start, count, format)
61
61
62
62
63 class FrontendWidget(HistoryConsoleWidget):
63 class FrontendWidget(HistoryConsoleWidget):
64 """ A Qt frontend for a generic Python kernel.
64 """ A Qt frontend for a generic Python kernel.
65 """
65 """
66
66
67 # ConsoleWidget interface.
68 tab_width = 4
69
70 # Emitted when an 'execute_reply' is received from the kernel.
67 # Emitted when an 'execute_reply' is received from the kernel.
71 executed = QtCore.pyqtSignal(object)
68 executed = QtCore.pyqtSignal(object)
72
69
73 #---------------------------------------------------------------------------
70 #---------------------------------------------------------------------------
74 # 'QObject' interface
71 # 'QObject' interface
75 #---------------------------------------------------------------------------
72 #---------------------------------------------------------------------------
76
73
77 def __init__(self, parent=None):
74 def __init__(self, parent=None):
78 super(FrontendWidget, self).__init__(parent)
75 super(FrontendWidget, self).__init__(parent)
79
76
80 # FrontendWidget protected variables.
77 # FrontendWidget protected variables.
81 self._call_tip_widget = CallTipWidget(self)
78 self._call_tip_widget = CallTipWidget(self)
82 self._completion_lexer = CompletionLexer(PythonLexer())
79 self._completion_lexer = CompletionLexer(PythonLexer())
83 self._hidden = True
80 self._hidden = True
84 self._highlighter = FrontendHighlighter(self)
81 self._highlighter = FrontendHighlighter(self)
85 self._input_splitter = InputSplitter(input_mode='replace')
82 self._input_splitter = InputSplitter(input_mode='replace')
86 self._kernel_manager = None
83 self._kernel_manager = None
87
84
88 # Configure the ConsoleWidget.
85 # Configure the ConsoleWidget.
86 self.tab_width = 4
89 self._set_continuation_prompt('... ')
87 self._set_continuation_prompt('... ')
90
88
91 self.document().contentsChange.connect(self._document_contents_change)
89 self.document().contentsChange.connect(self._document_contents_change)
92
90
93 #---------------------------------------------------------------------------
91 #---------------------------------------------------------------------------
94 # 'QWidget' interface
92 # 'QWidget' interface
95 #---------------------------------------------------------------------------
93 #---------------------------------------------------------------------------
96
94
97 def focusOutEvent(self, event):
95 def focusOutEvent(self, event):
98 """ Reimplemented to hide calltips.
96 """ Reimplemented to hide calltips.
99 """
97 """
100 self._call_tip_widget.hide()
98 self._call_tip_widget.hide()
101 super(FrontendWidget, self).focusOutEvent(event)
99 super(FrontendWidget, self).focusOutEvent(event)
102
100
103 def keyPressEvent(self, event):
101 def keyPressEvent(self, event):
104 """ Reimplemented to allow calltips to process events and to send
102 """ Reimplemented to allow calltips to process events and to send
105 signals to the kernel.
103 signals to the kernel.
106 """
104 """
107 if self._executing and event.key() == QtCore.Qt.Key_C and \
105 if self._executing and event.key() == QtCore.Qt.Key_C and \
108 self._control_down(event.modifiers()):
106 self._control_down(event.modifiers()):
109 self._interrupt_kernel()
107 self._interrupt_kernel()
110 else:
108 else:
111 if self._call_tip_widget.isVisible():
109 if self._call_tip_widget.isVisible():
112 self._call_tip_widget.keyPressEvent(event)
110 self._call_tip_widget.keyPressEvent(event)
113 super(FrontendWidget, self).keyPressEvent(event)
111 super(FrontendWidget, self).keyPressEvent(event)
114
112
115 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
116 # 'ConsoleWidget' abstract interface
114 # 'ConsoleWidget' abstract interface
117 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
118
116
119 def _is_complete(self, source, interactive):
117 def _is_complete(self, source, interactive):
120 """ Returns whether 'source' can be completely processed and a new
118 """ Returns whether 'source' can be completely processed and a new
121 prompt created. When triggered by an Enter/Return key press,
119 prompt created. When triggered by an Enter/Return key press,
122 'interactive' is True; otherwise, it is False.
120 'interactive' is True; otherwise, it is False.
123 """
121 """
124 complete = self._input_splitter.push(source.replace('\t', ' '))
122 complete = self._input_splitter.push(source.expandtabs(4))
125 if interactive:
123 if interactive:
126 complete = not self._input_splitter.push_accepts_more()
124 complete = not self._input_splitter.push_accepts_more()
127 return complete
125 return complete
128
126
129 def _execute(self, source, hidden):
127 def _execute(self, source, hidden):
130 """ Execute 'source'. If 'hidden', do not show any output.
128 """ Execute 'source'. If 'hidden', do not show any output.
131 """
129 """
132 self.kernel_manager.xreq_channel.execute(source)
130 self.kernel_manager.xreq_channel.execute(source)
133 self._hidden = hidden
131 self._hidden = hidden
134
132
135 def _prompt_started_hook(self):
133 def _prompt_started_hook(self):
136 """ Called immediately after a new prompt is displayed.
134 """ Called immediately after a new prompt is displayed.
137 """
135 """
138 if not self._reading:
136 if not self._reading:
139 self._highlighter.highlighting_on = True
137 self._highlighter.highlighting_on = True
140
138
141 # Auto-indent if this is a continuation prompt.
139 # Auto-indent if this is a continuation prompt.
142 if self._get_prompt_cursor().blockNumber() != \
140 if self._get_prompt_cursor().blockNumber() != \
143 self._get_end_cursor().blockNumber():
141 self._get_end_cursor().blockNumber():
144 spaces = self._input_splitter.indent_spaces
142 spaces = self._input_splitter.indent_spaces
145 self.appendPlainText('\t' * (spaces / self.tab_width))
143 self.appendPlainText('\t' * (spaces / self.tab_width))
146 self.appendPlainText(' ' * (spaces % self.tab_width))
144 self.appendPlainText(' ' * (spaces % self.tab_width))
147
145
148 def _prompt_finished_hook(self):
146 def _prompt_finished_hook(self):
149 """ Called immediately after a prompt is finished, i.e. when some input
147 """ Called immediately after a prompt is finished, i.e. when some input
150 will be processed and a new prompt displayed.
148 will be processed and a new prompt displayed.
151 """
149 """
152 if not self._reading:
150 if not self._reading:
153 self._highlighter.highlighting_on = False
151 self._highlighter.highlighting_on = False
154
152
155 def _tab_pressed(self):
153 def _tab_pressed(self):
156 """ Called when the tab key is pressed. Returns whether to continue
154 """ Called when the tab key is pressed. Returns whether to continue
157 processing the event.
155 processing the event.
158 """
156 """
159 self._keep_cursor_in_buffer()
157 self._keep_cursor_in_buffer()
160 cursor = self.textCursor()
158 cursor = self.textCursor()
161 return not self._complete()
159 return not self._complete()
162
160
163 #---------------------------------------------------------------------------
161 #---------------------------------------------------------------------------
164 # 'FrontendWidget' interface
162 # 'FrontendWidget' interface
165 #---------------------------------------------------------------------------
163 #---------------------------------------------------------------------------
166
164
167 def execute_file(self, path, hidden=False):
165 def execute_file(self, path, hidden=False):
168 """ Attempts to execute file with 'path'. If 'hidden', no output is
166 """ Attempts to execute file with 'path'. If 'hidden', no output is
169 shown.
167 shown.
170 """
168 """
171 self.execute('execfile("%s")' % path, hidden=hidden)
169 self.execute('execfile("%s")' % path, hidden=hidden)
172
170
173 def _get_kernel_manager(self):
171 def _get_kernel_manager(self):
174 """ Returns the current kernel manager.
172 """ Returns the current kernel manager.
175 """
173 """
176 return self._kernel_manager
174 return self._kernel_manager
177
175
178 def _set_kernel_manager(self, kernel_manager):
176 def _set_kernel_manager(self, kernel_manager):
179 """ Disconnect from the current kernel manager (if any) and set a new
177 """ Disconnect from the current kernel manager (if any) and set a new
180 kernel manager.
178 kernel manager.
181 """
179 """
182 # Disconnect the old kernel manager, if necessary.
180 # Disconnect the old kernel manager, if necessary.
183 if self._kernel_manager is not None:
181 if self._kernel_manager is not None:
184 self._kernel_manager.started_channels.disconnect(
182 self._kernel_manager.started_channels.disconnect(
185 self._started_channels)
183 self._started_channels)
186 self._kernel_manager.stopped_channels.disconnect(
184 self._kernel_manager.stopped_channels.disconnect(
187 self._stopped_channels)
185 self._stopped_channels)
188
186
189 # Disconnect the old kernel manager's channels.
187 # Disconnect the old kernel manager's channels.
190 sub = self._kernel_manager.sub_channel
188 sub = self._kernel_manager.sub_channel
191 xreq = self._kernel_manager.xreq_channel
189 xreq = self._kernel_manager.xreq_channel
192 rep = self._kernel_manager.rep_channel
190 rep = self._kernel_manager.rep_channel
193 sub.message_received.disconnect(self._handle_sub)
191 sub.message_received.disconnect(self._handle_sub)
194 xreq.execute_reply.disconnect(self._handle_execute_reply)
192 xreq.execute_reply.disconnect(self._handle_execute_reply)
195 xreq.complete_reply.disconnect(self._handle_complete_reply)
193 xreq.complete_reply.disconnect(self._handle_complete_reply)
196 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
194 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
197 rep.readline_requested.disconnect(self._handle_req)
195 rep.readline_requested.disconnect(self._handle_req)
198
196
199 # Handle the case where the old kernel manager is still listening.
197 # Handle the case where the old kernel manager is still listening.
200 if self._kernel_manager.channels_running:
198 if self._kernel_manager.channels_running:
201 self._stopped_channels()
199 self._stopped_channels()
202
200
203 # Set the new kernel manager.
201 # Set the new kernel manager.
204 self._kernel_manager = kernel_manager
202 self._kernel_manager = kernel_manager
205 if kernel_manager is None:
203 if kernel_manager is None:
206 return
204 return
207
205
208 # Connect the new kernel manager.
206 # Connect the new kernel manager.
209 kernel_manager.started_channels.connect(self._started_channels)
207 kernel_manager.started_channels.connect(self._started_channels)
210 kernel_manager.stopped_channels.connect(self._stopped_channels)
208 kernel_manager.stopped_channels.connect(self._stopped_channels)
211
209
212 # Connect the new kernel manager's channels.
210 # Connect the new kernel manager's channels.
213 sub = kernel_manager.sub_channel
211 sub = kernel_manager.sub_channel
214 xreq = kernel_manager.xreq_channel
212 xreq = kernel_manager.xreq_channel
215 rep = kernel_manager.rep_channel
213 rep = kernel_manager.rep_channel
216 sub.message_received.connect(self._handle_sub)
214 sub.message_received.connect(self._handle_sub)
217 xreq.execute_reply.connect(self._handle_execute_reply)
215 xreq.execute_reply.connect(self._handle_execute_reply)
218 xreq.complete_reply.connect(self._handle_complete_reply)
216 xreq.complete_reply.connect(self._handle_complete_reply)
219 xreq.object_info_reply.connect(self._handle_object_info_reply)
217 xreq.object_info_reply.connect(self._handle_object_info_reply)
220 rep.readline_requested.connect(self._handle_req)
218 rep.readline_requested.connect(self._handle_req)
221
219
222 # Handle the case where the kernel manager started channels before
220 # Handle the case where the kernel manager started channels before
223 # we connected.
221 # we connected.
224 if kernel_manager.channels_running:
222 if kernel_manager.channels_running:
225 self._started_channels()
223 self._started_channels()
226
224
227 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
225 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
228
226
229 #---------------------------------------------------------------------------
227 #---------------------------------------------------------------------------
230 # 'FrontendWidget' protected interface
228 # 'FrontendWidget' protected interface
231 #---------------------------------------------------------------------------
229 #---------------------------------------------------------------------------
232
230
233 def _call_tip(self):
231 def _call_tip(self):
234 """ Shows a call tip, if appropriate, at the current cursor location.
232 """ Shows a call tip, if appropriate, at the current cursor location.
235 """
233 """
236 # Decide if it makes sense to show a call tip
234 # Decide if it makes sense to show a call tip
237 cursor = self.textCursor()
235 cursor = self.textCursor()
238 cursor.movePosition(QtGui.QTextCursor.Left)
236 cursor.movePosition(QtGui.QTextCursor.Left)
239 document = self.document()
237 document = self.document()
240 if document.characterAt(cursor.position()).toAscii() != '(':
238 if document.characterAt(cursor.position()).toAscii() != '(':
241 return False
239 return False
242 context = self._get_context(cursor)
240 context = self._get_context(cursor)
243 if not context:
241 if not context:
244 return False
242 return False
245
243
246 # Send the metadata request to the kernel
244 # Send the metadata request to the kernel
247 name = '.'.join(context)
245 name = '.'.join(context)
248 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
246 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
249 self._calltip_pos = self.textCursor().position()
247 self._calltip_pos = self.textCursor().position()
250 return True
248 return True
251
249
252 def _complete(self):
250 def _complete(self):
253 """ Performs completion at the current cursor location.
251 """ Performs completion at the current cursor location.
254 """
252 """
255 # Decide if it makes sense to do completion
253 # Decide if it makes sense to do completion
256 context = self._get_context()
254 context = self._get_context()
257 if not context:
255 if not context:
258 return False
256 return False
259
257
260 # Send the completion request to the kernel
258 # Send the completion request to the kernel
261 text = '.'.join(context)
259 text = '.'.join(context)
262 self._complete_id = self.kernel_manager.xreq_channel.complete(
260 self._complete_id = self.kernel_manager.xreq_channel.complete(
263 text, self.input_buffer_cursor_line, self.input_buffer)
261 text, self.input_buffer_cursor_line, self.input_buffer)
264 self._complete_pos = self.textCursor().position()
262 self._complete_pos = self.textCursor().position()
265 return True
263 return True
266
264
267 def _get_banner(self):
265 def _get_banner(self):
268 """ Gets a banner to display at the beginning of a session.
266 """ Gets a banner to display at the beginning of a session.
269 """
267 """
270 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
268 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
271 '"license" for more information.'
269 '"license" for more information.'
272 return banner % (sys.version, sys.platform)
270 return banner % (sys.version, sys.platform)
273
271
274 def _get_context(self, cursor=None):
272 def _get_context(self, cursor=None):
275 """ Gets the context at the current cursor location.
273 """ Gets the context at the current cursor location.
276 """
274 """
277 if cursor is None:
275 if cursor is None:
278 cursor = self.textCursor()
276 cursor = self.textCursor()
279 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
277 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
280 QtGui.QTextCursor.KeepAnchor)
278 QtGui.QTextCursor.KeepAnchor)
281 text = str(cursor.selection().toPlainText())
279 text = str(cursor.selection().toPlainText())
282 return self._completion_lexer.get_context(text)
280 return self._completion_lexer.get_context(text)
283
281
284 def _interrupt_kernel(self):
282 def _interrupt_kernel(self):
285 """ Attempts to the interrupt the kernel.
283 """ Attempts to the interrupt the kernel.
286 """
284 """
287 if self.kernel_manager.has_kernel:
285 if self.kernel_manager.has_kernel:
288 self.kernel_manager.signal_kernel(signal.SIGINT)
286 self.kernel_manager.signal_kernel(signal.SIGINT)
289 else:
287 else:
290 self.appendPlainText('Kernel process is either remote or '
288 self.appendPlainText('Kernel process is either remote or '
291 'unspecified. Cannot interrupt.\n')
289 'unspecified. Cannot interrupt.\n')
292
290
293 def _show_interpreter_prompt(self):
291 def _show_interpreter_prompt(self):
294 """ Shows a prompt for the interpreter.
292 """ Shows a prompt for the interpreter.
295 """
293 """
296 self._show_prompt('>>> ')
294 self._show_prompt('>>> ')
297
295
298 #------ Signal handlers ----------------------------------------------------
296 #------ Signal handlers ----------------------------------------------------
299
297
300 def _started_channels(self):
298 def _started_channels(self):
301 """ Called when the kernel manager has started listening.
299 """ Called when the kernel manager has started listening.
302 """
300 """
303 self._reset()
301 self._reset()
304 self.appendPlainText(self._get_banner())
302 self.appendPlainText(self._get_banner())
305 self._show_interpreter_prompt()
303 self._show_interpreter_prompt()
306
304
307 def _stopped_channels(self):
305 def _stopped_channels(self):
308 """ Called when the kernel manager has stopped listening.
306 """ Called when the kernel manager has stopped listening.
309 """
307 """
310 # FIXME: Print a message here?
308 # FIXME: Print a message here?
311 pass
309 pass
312
310
313 def _document_contents_change(self, position, removed, added):
311 def _document_contents_change(self, position, removed, added):
314 """ Called whenever the document's content changes. Display a calltip
312 """ Called whenever the document's content changes. Display a calltip
315 if appropriate.
313 if appropriate.
316 """
314 """
317 # Calculate where the cursor should be *after* the change:
315 # Calculate where the cursor should be *after* the change:
318 position += added
316 position += added
319
317
320 document = self.document()
318 document = self.document()
321 if position == self.textCursor().position():
319 if position == self.textCursor().position():
322 self._call_tip()
320 self._call_tip()
323
321
324 def _handle_req(self, req):
322 def _handle_req(self, req):
325 # Make sure that all output from the SUB channel has been processed
323 # Make sure that all output from the SUB channel has been processed
326 # before entering readline mode.
324 # before entering readline mode.
327 self.kernel_manager.sub_channel.flush()
325 self.kernel_manager.sub_channel.flush()
328
326
329 def callback(line):
327 def callback(line):
330 self.kernel_manager.rep_channel.readline(line)
328 self.kernel_manager.rep_channel.readline(line)
331 self._readline(callback=callback)
329 self._readline(callback=callback)
332
330
333 def _handle_sub(self, omsg):
331 def _handle_sub(self, omsg):
334 if self._hidden:
332 if self._hidden:
335 return
333 return
336 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
334 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
337 if handler is not None:
335 if handler is not None:
338 handler(omsg)
336 handler(omsg)
339
337
340 def _handle_pyout(self, omsg):
338 def _handle_pyout(self, omsg):
341 self.appendPlainText(omsg['content']['data'] + '\n')
339 self.appendPlainText(omsg['content']['data'] + '\n')
342
340
343 def _handle_stream(self, omsg):
341 def _handle_stream(self, omsg):
344 self.appendPlainText(omsg['content']['data'])
342 self.appendPlainText(omsg['content']['data'])
345 self.moveCursor(QtGui.QTextCursor.End)
343 self.moveCursor(QtGui.QTextCursor.End)
346
344
347 def _handle_execute_reply(self, reply):
345 def _handle_execute_reply(self, reply):
348 if self._hidden:
346 if self._hidden:
349 return
347 return
350
348
351 # Make sure that all output from the SUB channel has been processed
349 # Make sure that all output from the SUB channel has been processed
352 # before writing a new prompt.
350 # before writing a new prompt.
353 self.kernel_manager.sub_channel.flush()
351 self.kernel_manager.sub_channel.flush()
354
352
355 status = reply['content']['status']
353 status = reply['content']['status']
356 if status == 'error':
354 if status == 'error':
357 self._handle_execute_error(reply)
355 self._handle_execute_error(reply)
358 elif status == 'aborted':
356 elif status == 'aborted':
359 text = "ERROR: ABORTED\n"
357 text = "ERROR: ABORTED\n"
360 self.appendPlainText(text)
358 self.appendPlainText(text)
361 self._hidden = True
359 self._hidden = True
362 self._show_interpreter_prompt()
360 self._show_interpreter_prompt()
363 self.executed.emit(reply)
361 self.executed.emit(reply)
364
362
365 def _handle_execute_error(self, reply):
363 def _handle_execute_error(self, reply):
366 content = reply['content']
364 content = reply['content']
367 traceback = ''.join(content['traceback'])
365 traceback = ''.join(content['traceback'])
368 self.appendPlainText(traceback)
366 self.appendPlainText(traceback)
369
367
370 def _handle_complete_reply(self, rep):
368 def _handle_complete_reply(self, rep):
371 cursor = self.textCursor()
369 cursor = self.textCursor()
372 if rep['parent_header']['msg_id'] == self._complete_id and \
370 if rep['parent_header']['msg_id'] == self._complete_id and \
373 cursor.position() == self._complete_pos:
371 cursor.position() == self._complete_pos:
374 text = '.'.join(self._get_context())
372 text = '.'.join(self._get_context())
375 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
373 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
376 self._complete_with_items(cursor, rep['content']['matches'])
374 self._complete_with_items(cursor, rep['content']['matches'])
377
375
378 def _handle_object_info_reply(self, rep):
376 def _handle_object_info_reply(self, rep):
379 cursor = self.textCursor()
377 cursor = self.textCursor()
380 if rep['parent_header']['msg_id'] == self._calltip_id and \
378 if rep['parent_header']['msg_id'] == self._calltip_id and \
381 cursor.position() == self._calltip_pos:
379 cursor.position() == self._calltip_pos:
382 doc = rep['content']['docstring']
380 doc = rep['content']['docstring']
383 if doc:
381 if doc:
384 self._call_tip_widget.show_docstring(doc)
382 self._call_tip_widget.show_docstring(doc)
General Comments 0
You need to be logged in to leave comments. Login now