##// END OF EJS Templates
Made use of plain text consistent.
epatters -
Show More
@@ -1,74 +1,74 b''
1 1 # System library imports
2 2 from pygments.token import Token, is_token_subtype
3 3
4 4
5 5 class CompletionLexer(object):
6 6 """ Uses Pygments and some auxillary information to lex code snippets for
7 7 symbol contexts.
8 8 """
9 9
10 10 # Maps Lexer names to a list of possible name separators
11 11 separator_map = { 'C' : [ '.', '->' ],
12 12 'C++' : [ '.', '->', '::' ],
13 13 'Python' : [ '.' ] }
14 14
15 15 def __init__(self, lexer):
16 16 """ Create a CompletionLexer using the specified Pygments lexer.
17 17 """
18 18 self.lexer = lexer
19 19
20 20 def get_context(self, string):
21 21 """ Assuming the cursor is at the end of the specified string, get the
22 22 context (a list of names) for the symbol at cursor position.
23 23 """
24 24 context = []
25 25 reversed_tokens = list(self._lexer.get_tokens(string))
26 26 reversed_tokens.reverse()
27 27
28 28 # Pygments often tacks on a newline when none is specified in the input.
29 29 # Remove this newline.
30 30 if reversed_tokens and reversed_tokens[0][1].endswith('\n') and \
31 31 not string.endswith('\n'):
32 32 reversed_tokens.pop(0)
33 33
34 current_op = unicode()
34 current_op = ''
35 35 for token, text in reversed_tokens:
36 36
37 37 if is_token_subtype(token, Token.Name):
38 38
39 39 # Handle a trailing separator, e.g 'foo.bar.'
40 40 if current_op in self._name_separators:
41 41 if not context:
42 context.insert(0, unicode())
42 context.insert(0, '')
43 43
44 44 # Handle non-separator operators and punction.
45 45 elif current_op:
46 46 break
47 47
48 48 context.insert(0, text)
49 current_op = unicode()
49 current_op = ''
50 50
51 51 # Pygments doesn't understand that, e.g., '->' is a single operator
52 52 # in C++. This is why we have to build up an operator from
53 53 # potentially several tokens.
54 54 elif token is Token.Operator or token is Token.Punctuation:
55 55 current_op = text + current_op
56 56
57 57 # Break on anything that is not a Operator, Punctuation, or Name.
58 58 else:
59 59 break
60 60
61 61 return context
62 62
63 63 def get_lexer(self, lexer):
64 64 return self._lexer
65 65
66 66 def set_lexer(self, lexer, name_separators=None):
67 67 self._lexer = lexer
68 68 if name_separators is None:
69 69 self._name_separators = self.separator_map.get(lexer.name, ['.'])
70 70 else:
71 71 self._name_separators = list(name_separators)
72 72
73 73 lexer = property(get_lexer, set_lexer)
74 74
@@ -1,124 +1,124 b''
1 1 # System library imports
2 2 from PyQt4 import QtCore, QtGui
3 3
4 4
5 5 class CompletionWidget(QtGui.QListWidget):
6 6 """ A widget for GUI tab completion.
7 7 """
8 8
9 9 #--------------------------------------------------------------------------
10 10 # 'QWidget' interface
11 11 #--------------------------------------------------------------------------
12 12
13 13 def __init__(self, parent):
14 14 """ Create a completion widget that is attached to the specified Qt
15 15 text edit widget.
16 16 """
17 17 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 18 QtGui.QListWidget.__init__(self, parent)
19 19
20 20 self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
21 21 self.setAttribute(QtCore.Qt.WA_StaticContents)
22 22
23 23 # Ensure that parent keeps focus when widget is displayed.
24 24 self.setFocusProxy(parent)
25 25
26 26 self.setFrameShadow(QtGui.QFrame.Plain)
27 27 self.setFrameShape(QtGui.QFrame.StyledPanel)
28 28
29 29 self.itemActivated.connect(self._complete_current)
30 30
31 31 def hideEvent(self, event):
32 32 """ Reimplemented to disconnect the cursor movement handler.
33 33 """
34 34 QtGui.QListWidget.hideEvent(self, event)
35 35 try:
36 36 self.parent().cursorPositionChanged.disconnect(self._update_current)
37 37 except TypeError:
38 38 pass
39 39
40 40 def keyPressEvent(self, event):
41 41 """ Reimplemented to update the list.
42 42 """
43 43 key, text = event.key(), event.text()
44 44
45 45 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
46 46 QtCore.Qt.Key_Tab):
47 47 self._complete_current()
48 48 event.accept()
49 49
50 50 elif key == QtCore.Qt.Key_Escape:
51 51 self.hide()
52 52 event.accept()
53 53
54 54 elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
55 55 QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
56 56 QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
57 57 QtGui.QListWidget.keyPressEvent(self, event)
58 58 event.accept()
59 59
60 60 else:
61 61 event.ignore()
62 62
63 63 def showEvent(self, event):
64 64 """ Reimplemented to connect the cursor movement handler.
65 65 """
66 66 QtGui.QListWidget.showEvent(self, event)
67 67 self.parent().cursorPositionChanged.connect(self._update_current)
68 68
69 69 #--------------------------------------------------------------------------
70 70 # 'CompletionWidget' interface
71 71 #--------------------------------------------------------------------------
72 72
73 73 def show_items(self, cursor, items):
74 74 """ Shows the completion widget with 'items' at the position specified
75 75 by 'cursor'.
76 76 """
77 77 text_edit = self.parent()
78 78 point = text_edit.cursorRect(cursor).bottomRight()
79 79 point = text_edit.mapToGlobal(point)
80 80 screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
81 81 if screen_rect.size().height() - point.y() - self.height() < 0:
82 82 point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
83 83 point.setY(point.y() - self.height())
84 84 self.move(point)
85 85
86 86 self._start_position = cursor.position()
87 87 self.clear()
88 88 self.addItems(items)
89 89 self.setCurrentRow(0)
90 90 self.show()
91 91
92 92 #--------------------------------------------------------------------------
93 93 # Protected interface
94 94 #--------------------------------------------------------------------------
95 95
96 96 def _complete_current(self):
97 97 """ Perform the completion with the currently selected item.
98 98 """
99 99 self._current_text_cursor().insertText(self.currentItem().text())
100 100 self.hide()
101 101
102 102 def _current_text_cursor(self):
103 103 """ Returns a cursor with text between the start position and the
104 104 current position selected.
105 105 """
106 106 cursor = self.parent().textCursor()
107 107 if cursor.position() >= self._start_position:
108 108 cursor.setPosition(self._start_position,
109 109 QtGui.QTextCursor.KeepAnchor)
110 110 return cursor
111 111
112 112 def _update_current(self):
113 113 """ Updates the current item based on the current text.
114 114 """
115 prefix = self._current_text_cursor().selectedText()
115 prefix = self._current_text_cursor().selection().toPlainText()
116 116 if prefix:
117 117 items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
118 118 QtCore.Qt.MatchCaseSensitive))
119 119 if items:
120 120 self.setCurrentItem(items[0])
121 121 else:
122 122 self.hide()
123 123 else:
124 124 self.hide()
@@ -1,380 +1,380 b''
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8 import zmq
9 9
10 10 # Local imports
11 11 from IPython.core.inputsplitter import InputSplitter
12 12 from call_tip_widget import CallTipWidget
13 13 from completion_lexer import CompletionLexer
14 14 from console_widget import HistoryConsoleWidget
15 15 from pygments_highlighter import PygmentsHighlighter
16 16
17 17
18 18 class FrontendHighlighter(PygmentsHighlighter):
19 19 """ A PygmentsHighlighter that can be turned on and off and that ignores
20 20 prompts.
21 21 """
22 22
23 23 def __init__(self, frontend):
24 24 super(FrontendHighlighter, self).__init__(frontend.document())
25 25 self._current_offset = 0
26 26 self._frontend = frontend
27 27 self.highlighting_on = False
28 28
29 29 def highlightBlock(self, qstring):
30 30 """ Highlight a block of text. Reimplemented to highlight selectively.
31 31 """
32 32 if not self.highlighting_on:
33 33 return
34 34
35 35 # The input to this function is unicode string that may contain
36 36 # paragraph break characters, non-breaking spaces, etc. Here we acquire
37 37 # the string as plain text so we can compare it.
38 38 current_block = self.currentBlock()
39 39 string = self._frontend._get_block_plain_text(current_block)
40 40
41 41 # Decide whether to check for the regular or continuation prompt.
42 42 if current_block.contains(self._frontend._prompt_pos):
43 43 prompt = self._frontend._prompt
44 44 else:
45 45 prompt = self._frontend._continuation_prompt
46 46
47 47 # Don't highlight the part of the string that contains the prompt.
48 48 if string.startswith(prompt):
49 49 self._current_offset = len(prompt)
50 50 qstring.remove(0, len(prompt))
51 51 else:
52 52 self._current_offset = 0
53 53
54 54 PygmentsHighlighter.highlightBlock(self, qstring)
55 55
56 56 def setFormat(self, start, count, format):
57 57 """ Reimplemented to highlight selectively.
58 58 """
59 59 start += self._current_offset
60 60 PygmentsHighlighter.setFormat(self, start, count, format)
61 61
62 62
63 63 class FrontendWidget(HistoryConsoleWidget):
64 64 """ A Qt frontend for a generic Python kernel.
65 65 """
66 66
67 67 # Emitted when an 'execute_reply' is received from the kernel.
68 68 executed = QtCore.pyqtSignal(object)
69 69
70 70 #---------------------------------------------------------------------------
71 71 # 'QObject' interface
72 72 #---------------------------------------------------------------------------
73 73
74 74 def __init__(self, parent=None):
75 75 super(FrontendWidget, self).__init__(parent)
76 76
77 77 # FrontendWidget protected variables.
78 78 self._call_tip_widget = CallTipWidget(self)
79 79 self._completion_lexer = CompletionLexer(PythonLexer())
80 80 self._hidden = True
81 81 self._highlighter = FrontendHighlighter(self)
82 82 self._input_splitter = InputSplitter(input_mode='replace')
83 83 self._kernel_manager = None
84 84
85 85 # Configure the ConsoleWidget.
86 86 self._set_continuation_prompt('... ')
87 87
88 88 self.document().contentsChange.connect(self._document_contents_change)
89 89
90 90 #---------------------------------------------------------------------------
91 91 # 'QWidget' interface
92 92 #---------------------------------------------------------------------------
93 93
94 94 def focusOutEvent(self, event):
95 95 """ Reimplemented to hide calltips.
96 96 """
97 97 self._call_tip_widget.hide()
98 98 super(FrontendWidget, self).focusOutEvent(event)
99 99
100 100 def keyPressEvent(self, event):
101 101 """ Reimplemented to allow calltips to process events and to send
102 102 signals to the kernel.
103 103 """
104 104 if self._executing and event.key() == QtCore.Qt.Key_C and \
105 105 self._control_down(event.modifiers()):
106 106 self._interrupt_kernel()
107 107 else:
108 108 if self._call_tip_widget.isVisible():
109 109 self._call_tip_widget.keyPressEvent(event)
110 110 super(FrontendWidget, self).keyPressEvent(event)
111 111
112 112 #---------------------------------------------------------------------------
113 113 # 'ConsoleWidget' abstract interface
114 114 #---------------------------------------------------------------------------
115 115
116 116 def _is_complete(self, source, interactive):
117 117 """ Returns whether 'source' can be completely processed and a new
118 118 prompt created. When triggered by an Enter/Return key press,
119 119 'interactive' is True; otherwise, it is False.
120 120 """
121 121 complete = self._input_splitter.push(source.replace('\t', ' '))
122 122 if interactive:
123 123 complete = not self._input_splitter.push_accepts_more()
124 124 return complete
125 125
126 126 def _execute(self, source, hidden):
127 127 """ Execute 'source'. If 'hidden', do not show any output.
128 128 """
129 129 self.kernel_manager.xreq_channel.execute(source)
130 130 self._hidden = hidden
131 131
132 132 def _prompt_started_hook(self):
133 133 """ Called immediately after a new prompt is displayed.
134 134 """
135 135 if not self._reading:
136 136 self._highlighter.highlighting_on = True
137 137
138 138 # Auto-indent if this is a continuation prompt.
139 139 if self._get_prompt_cursor().blockNumber() != \
140 140 self._get_end_cursor().blockNumber():
141 141 spaces = self._input_splitter.indent_spaces
142 142 self.appendPlainText('\t' * (spaces / 4) + ' ' * (spaces % 4))
143 143
144 144 def _prompt_finished_hook(self):
145 145 """ Called immediately after a prompt is finished, i.e. when some input
146 146 will be processed and a new prompt displayed.
147 147 """
148 148 if not self._reading:
149 149 self._highlighter.highlighting_on = False
150 150
151 151 def _tab_pressed(self):
152 152 """ Called when the tab key is pressed. Returns whether to continue
153 153 processing the event.
154 154 """
155 155 self._keep_cursor_in_buffer()
156 156 cursor = self.textCursor()
157 157 return not self._complete()
158 158
159 159 #---------------------------------------------------------------------------
160 160 # 'FrontendWidget' interface
161 161 #---------------------------------------------------------------------------
162 162
163 163 def execute_file(self, path, hidden=False):
164 164 """ Attempts to execute file with 'path'. If 'hidden', no output is
165 165 shown.
166 166 """
167 167 self.execute('execfile("%s")' % path, hidden=hidden)
168 168
169 169 def _get_kernel_manager(self):
170 170 """ Returns the current kernel manager.
171 171 """
172 172 return self._kernel_manager
173 173
174 174 def _set_kernel_manager(self, kernel_manager):
175 175 """ Disconnect from the current kernel manager (if any) and set a new
176 176 kernel manager.
177 177 """
178 178 # Disconnect the old kernel manager, if necessary.
179 179 if self._kernel_manager is not None:
180 180 self._kernel_manager.started_channels.disconnect(
181 181 self._started_channels)
182 182 self._kernel_manager.stopped_channels.disconnect(
183 183 self._stopped_channels)
184 184
185 185 # Disconnect the old kernel manager's channels.
186 186 sub = self._kernel_manager.sub_channel
187 187 xreq = self._kernel_manager.xreq_channel
188 188 rep = self._kernel_manager.rep_channel
189 189 sub.message_received.disconnect(self._handle_sub)
190 190 xreq.execute_reply.disconnect(self._handle_execute_reply)
191 191 xreq.complete_reply.disconnect(self._handle_complete_reply)
192 192 xreq.object_info_reply.disconnect(self._handle_object_info_reply)
193 193 rep.readline_requested.disconnect(self._handle_req)
194 194
195 195 # Handle the case where the old kernel manager is still listening.
196 196 if self._kernel_manager.channels_running:
197 197 self._stopped_channels()
198 198
199 199 # Set the new kernel manager.
200 200 self._kernel_manager = kernel_manager
201 201 if kernel_manager is None:
202 202 return
203 203
204 204 # Connect the new kernel manager.
205 205 kernel_manager.started_channels.connect(self._started_channels)
206 206 kernel_manager.stopped_channels.connect(self._stopped_channels)
207 207
208 208 # Connect the new kernel manager's channels.
209 209 sub = kernel_manager.sub_channel
210 210 xreq = kernel_manager.xreq_channel
211 211 rep = kernel_manager.rep_channel
212 212 sub.message_received.connect(self._handle_sub)
213 213 xreq.execute_reply.connect(self._handle_execute_reply)
214 214 xreq.complete_reply.connect(self._handle_complete_reply)
215 215 xreq.object_info_reply.connect(self._handle_object_info_reply)
216 216 rep.readline_requested.connect(self._handle_req)
217 217
218 218 # Handle the case where the kernel manager started channels before
219 219 # we connected.
220 220 if kernel_manager.channels_running:
221 221 self._started_channels()
222 222
223 223 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
224 224
225 225 #---------------------------------------------------------------------------
226 226 # 'FrontendWidget' protected interface
227 227 #---------------------------------------------------------------------------
228 228
229 229 def _call_tip(self):
230 230 """ Shows a call tip, if appropriate, at the current cursor location.
231 231 """
232 232 # Decide if it makes sense to show a call tip
233 233 cursor = self.textCursor()
234 234 cursor.movePosition(QtGui.QTextCursor.Left)
235 235 document = self.document()
236 236 if document.characterAt(cursor.position()).toAscii() != '(':
237 237 return False
238 238 context = self._get_context(cursor)
239 239 if not context:
240 240 return False
241 241
242 242 # Send the metadata request to the kernel
243 243 name = '.'.join(context)
244 244 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
245 245 self._calltip_pos = self.textCursor().position()
246 246 return True
247 247
248 248 def _complete(self):
249 249 """ Performs completion at the current cursor location.
250 250 """
251 251 # Decide if it makes sense to do completion
252 252 context = self._get_context()
253 253 if not context:
254 254 return False
255 255
256 256 # Send the completion request to the kernel
257 257 text = '.'.join(context)
258 258 self._complete_id = self.kernel_manager.xreq_channel.complete(
259 259 text, self.input_buffer_cursor_line, self.input_buffer)
260 260 self._complete_pos = self.textCursor().position()
261 261 return True
262 262
263 263 def _get_banner(self):
264 264 """ Gets a banner to display at the beginning of a session.
265 265 """
266 266 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
267 267 '"license" for more information.'
268 268 return banner % (sys.version, sys.platform)
269 269
270 270 def _get_context(self, cursor=None):
271 271 """ Gets the context at the current cursor location.
272 272 """
273 273 if cursor is None:
274 274 cursor = self.textCursor()
275 275 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
276 276 QtGui.QTextCursor.KeepAnchor)
277 text = unicode(cursor.selectedText())
277 text = str(cursor.selection().toPlainText())
278 278 return self._completion_lexer.get_context(text)
279 279
280 280 def _interrupt_kernel(self):
281 281 """ Attempts to the interrupt the kernel.
282 282 """
283 283 if self.kernel_manager.has_kernel:
284 284 self.kernel_manager.signal_kernel(signal.SIGINT)
285 285 else:
286 286 self.appendPlainText('Kernel process is either remote or '
287 287 'unspecified. Cannot interrupt.\n')
288 288
289 289 def _show_interpreter_prompt(self):
290 290 """ Shows a prompt for the interpreter.
291 291 """
292 292 self._show_prompt('>>> ')
293 293
294 294 #------ Signal handlers ----------------------------------------------------
295 295
296 296 def _started_channels(self):
297 297 """ Called when the kernel manager has started listening.
298 298 """
299 299 self._reset()
300 300 self.appendPlainText(self._get_banner())
301 301 self._show_interpreter_prompt()
302 302
303 303 def _stopped_channels(self):
304 304 """ Called when the kernel manager has stopped listening.
305 305 """
306 306 # FIXME: Print a message here?
307 307 pass
308 308
309 309 def _document_contents_change(self, position, removed, added):
310 310 """ Called whenever the document's content changes. Display a calltip
311 311 if appropriate.
312 312 """
313 313 # Calculate where the cursor should be *after* the change:
314 314 position += added
315 315
316 316 document = self.document()
317 317 if position == self.textCursor().position():
318 318 self._call_tip()
319 319
320 320 def _handle_req(self, req):
321 321 # Make sure that all output from the SUB channel has been processed
322 322 # before entering readline mode.
323 323 self.kernel_manager.sub_channel.flush()
324 324
325 325 def callback(line):
326 326 self.kernel_manager.rep_channel.readline(line)
327 327 self._readline(callback=callback)
328 328
329 329 def _handle_sub(self, omsg):
330 330 if self._hidden:
331 331 return
332 332 handler = getattr(self, '_handle_%s' % omsg['msg_type'], None)
333 333 if handler is not None:
334 334 handler(omsg)
335 335
336 336 def _handle_pyout(self, omsg):
337 337 self.appendPlainText(omsg['content']['data'] + '\n')
338 338
339 339 def _handle_stream(self, omsg):
340 340 self.appendPlainText(omsg['content']['data'])
341 341 self.moveCursor(QtGui.QTextCursor.End)
342 342
343 343 def _handle_execute_reply(self, reply):
344 344 if self._hidden:
345 345 return
346 346
347 347 # Make sure that all output from the SUB channel has been processed
348 348 # before writing a new prompt.
349 349 self.kernel_manager.sub_channel.flush()
350 350
351 351 status = reply['content']['status']
352 352 if status == 'error':
353 353 self._handle_execute_error(reply)
354 354 elif status == 'aborted':
355 355 text = "ERROR: ABORTED\n"
356 356 self.appendPlainText(text)
357 357 self._hidden = True
358 358 self._show_interpreter_prompt()
359 359 self.executed.emit(reply)
360 360
361 361 def _handle_execute_error(self, reply):
362 362 content = reply['content']
363 363 traceback = ''.join(content['traceback'])
364 364 self.appendPlainText(traceback)
365 365
366 366 def _handle_complete_reply(self, rep):
367 367 cursor = self.textCursor()
368 368 if rep['parent_header']['msg_id'] == self._complete_id and \
369 369 cursor.position() == self._complete_pos:
370 370 text = '.'.join(self._get_context())
371 371 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
372 372 self._complete_with_items(cursor, rep['content']['matches'])
373 373
374 374 def _handle_object_info_reply(self, rep):
375 375 cursor = self.textCursor()
376 376 if rep['parent_header']['msg_id'] == self._calltip_id and \
377 377 cursor.position() == self._calltip_pos:
378 378 doc = rep['content']['docstring']
379 379 if doc:
380 380 self._call_tip_widget.show_docstring(doc)
General Comments 0
You need to be logged in to leave comments. Login now