##// END OF EJS Templates
First cut at a generic bracket matcher for Q[Plain]TextEdits.
epatters -
Show More
@@ -0,0 +1,101 b''
1 """ Provides bracket matching for Q[Plain]TextEdit widgets.
2 """
3
4 # System library imports
5 from PyQt4 import QtCore, QtGui
6
7
8 class BracketMatcher(QtCore.QObject):
9 """ Matches square brackets, braces, and parentheses based on cursor
10 position.
11 """
12
13 # Protected class variables.
14 _opening_map = { '(':')', '{':'}', '[':']' }
15 _closing_map = { ')':'(', '}':'{', ']':'[' }
16
17 #--------------------------------------------------------------------------
18 # 'QObject' interface
19 #--------------------------------------------------------------------------
20
21 def __init__(self, parent, multiline=True):
22 """ Create a call tip manager that is attached to the specified Qt
23 text edit widget.
24 """
25 assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
26 QtCore.QObject.__init__(self, parent)
27
28 # The format to apply to matching brackets.
29 self.format = QtGui.QTextCharFormat()
30 self.format.setBackground(QtGui.QColor('silver'))
31
32 parent.cursorPositionChanged.connect(self._cursor_position_changed)
33
34 #--------------------------------------------------------------------------
35 # Protected interface
36 #--------------------------------------------------------------------------
37
38 def _find_match(self, position):
39 """ Given a valid position in the text document, try to find the
40 position of the matching bracket. Returns -1 if unsuccessful.
41 """
42 # Decide what character to search for and what direction to search in.
43 document = self.parent().document()
44 qchar = document.characterAt(position)
45 start_char = qchar.toAscii()
46 search_char = self._opening_map.get(start_char)
47 if search_char:
48 increment = 1
49 else:
50 search_char = self._closing_map.get(start_char)
51 if search_char:
52 increment = -1
53 else:
54 return -1
55
56 # Search for the character.
57 depth = 0
58 while position >= 0 and position < document.characterCount():
59 char = qchar.toAscii()
60 if char == start_char:
61 depth += 1
62 elif char == search_char:
63 depth -= 1
64 if depth == 0:
65 break
66 position += increment
67 qchar = document.characterAt(position)
68 else:
69 position = -1
70 return position
71
72 def _selection_for_character(self, position):
73 """ Convenience method for selecting a character.
74 """
75 selection = QtGui.QTextEdit.ExtraSelection()
76 cursor = self.parent().textCursor()
77 cursor.setPosition(position)
78 cursor.movePosition(QtGui.QTextCursor.NextCharacter,
79 QtGui.QTextCursor.KeepAnchor)
80 selection.cursor = cursor
81 selection.format = self.format
82 return selection
83
84 #------ Signal handlers ----------------------------------------------------
85
86 def _cursor_position_changed(self):
87 """ Updates the document formatting based on the new cursor position.
88 """
89 # Clear out the old formatting.
90 text_edit = self.parent()
91 text_edit.setExtraSelections([])
92
93 # Attempt to match a bracket for the new cursor position.
94 cursor = text_edit.textCursor()
95 if not cursor.hasSelection():
96 position = cursor.position() - 1
97 match_position = self._find_match(position)
98 if match_position != -1:
99 extra_selections = [ self._selection_for_character(pos)
100 for pos in (position, match_position) ]
101 text_edit.setExtraSelections(extra_selections)
@@ -1,419 +1,420 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
8
9 # Local imports
9 # Local imports
10 from IPython.core.inputsplitter import InputSplitter
10 from IPython.core.inputsplitter import InputSplitter
11 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
11 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 from IPython.utils.traitlets import Bool, Type
12 from IPython.utils.traitlets import Bool
13 from bracket_matcher import BracketMatcher
13 from call_tip_widget import CallTipWidget
14 from call_tip_widget import CallTipWidget
14 from completion_lexer import CompletionLexer
15 from completion_lexer import CompletionLexer
15 from console_widget import HistoryConsoleWidget
16 from console_widget import HistoryConsoleWidget
16 from pygments_highlighter import PygmentsHighlighter
17 from pygments_highlighter import PygmentsHighlighter
17
18
18
19
19 class FrontendHighlighter(PygmentsHighlighter):
20 class FrontendHighlighter(PygmentsHighlighter):
20 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 """ A PygmentsHighlighter that can be turned on and off and that ignores
21 prompts.
22 prompts.
22 """
23 """
23
24
24 def __init__(self, frontend):
25 def __init__(self, frontend):
25 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 super(FrontendHighlighter, self).__init__(frontend._control.document())
26 self._current_offset = 0
27 self._current_offset = 0
27 self._frontend = frontend
28 self._frontend = frontend
28 self.highlighting_on = False
29 self.highlighting_on = False
29
30
30 def highlightBlock(self, qstring):
31 def highlightBlock(self, qstring):
31 """ Highlight a block of text. Reimplemented to highlight selectively.
32 """ Highlight a block of text. Reimplemented to highlight selectively.
32 """
33 """
33 if not self.highlighting_on:
34 if not self.highlighting_on:
34 return
35 return
35
36
36 # The input to this function is unicode string that may contain
37 # The input to this function is unicode string that may contain
37 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 # paragraph break characters, non-breaking spaces, etc. Here we acquire
38 # the string as plain text so we can compare it.
39 # the string as plain text so we can compare it.
39 current_block = self.currentBlock()
40 current_block = self.currentBlock()
40 string = self._frontend._get_block_plain_text(current_block)
41 string = self._frontend._get_block_plain_text(current_block)
41
42
42 # Decide whether to check for the regular or continuation prompt.
43 # Decide whether to check for the regular or continuation prompt.
43 if current_block.contains(self._frontend._prompt_pos):
44 if current_block.contains(self._frontend._prompt_pos):
44 prompt = self._frontend._prompt
45 prompt = self._frontend._prompt
45 else:
46 else:
46 prompt = self._frontend._continuation_prompt
47 prompt = self._frontend._continuation_prompt
47
48
48 # Don't highlight the part of the string that contains the prompt.
49 # Don't highlight the part of the string that contains the prompt.
49 if string.startswith(prompt):
50 if string.startswith(prompt):
50 self._current_offset = len(prompt)
51 self._current_offset = len(prompt)
51 qstring.remove(0, len(prompt))
52 qstring.remove(0, len(prompt))
52 else:
53 else:
53 self._current_offset = 0
54 self._current_offset = 0
54
55
55 PygmentsHighlighter.highlightBlock(self, qstring)
56 PygmentsHighlighter.highlightBlock(self, qstring)
56
57
57 def rehighlightBlock(self, block):
58 def rehighlightBlock(self, block):
58 """ Reimplemented to temporarily enable highlighting if disabled.
59 """ Reimplemented to temporarily enable highlighting if disabled.
59 """
60 """
60 old = self.highlighting_on
61 old = self.highlighting_on
61 self.highlighting_on = True
62 self.highlighting_on = True
62 super(FrontendHighlighter, self).rehighlightBlock(block)
63 super(FrontendHighlighter, self).rehighlightBlock(block)
63 self.highlighting_on = old
64 self.highlighting_on = old
64
65
65 def setFormat(self, start, count, format):
66 def setFormat(self, start, count, format):
66 """ Reimplemented to highlight selectively.
67 """ Reimplemented to highlight selectively.
67 """
68 """
68 start += self._current_offset
69 start += self._current_offset
69 PygmentsHighlighter.setFormat(self, start, count, format)
70 PygmentsHighlighter.setFormat(self, start, count, format)
70
71
71
72
72 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
73 """ A Qt frontend for a generic Python kernel.
74 """ A Qt frontend for a generic Python kernel.
74 """
75 """
75
76
76 # An option and corresponding signal for overriding the default kernel
77 # An option and corresponding signal for overriding the default kernel
77 # interrupt behavior.
78 # interrupt behavior.
78 custom_interrupt = Bool(False)
79 custom_interrupt = Bool(False)
79 custom_interrupt_requested = QtCore.pyqtSignal()
80 custom_interrupt_requested = QtCore.pyqtSignal()
80
81
81 # An option and corresponding signal for overriding the default kernel
82 # An option and corresponding signal for overriding the default kernel
82 # restart behavior.
83 # restart behavior.
83 custom_restart = Bool(False)
84 custom_restart = Bool(False)
84 custom_restart_requested = QtCore.pyqtSignal()
85 custom_restart_requested = QtCore.pyqtSignal()
85
86
86 # Emitted when an 'execute_reply' has been received from the kernel and
87 # Emitted when an 'execute_reply' has been received from the kernel and
87 # processed by the FrontendWidget.
88 # processed by the FrontendWidget.
88 executed = QtCore.pyqtSignal(object)
89 executed = QtCore.pyqtSignal(object)
89
90
90 # Protected class variables.
91 # Protected class variables.
91 _highlighter_class = Type(FrontendHighlighter)
92 _input_splitter_class = InputSplitter
92 _input_splitter_class = Type(InputSplitter)
93
93
94 #---------------------------------------------------------------------------
94 #---------------------------------------------------------------------------
95 # 'object' interface
95 # 'object' interface
96 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
97
97
98 def __init__(self, *args, **kw):
98 def __init__(self, *args, **kw):
99 super(FrontendWidget, self).__init__(*args, **kw)
99 super(FrontendWidget, self).__init__(*args, **kw)
100
100
101 # FrontendWidget protected variables.
101 # FrontendWidget protected variables.
102 self._bracket_matcher = BracketMatcher(self._control)
102 self._call_tip_widget = CallTipWidget(self._control)
103 self._call_tip_widget = CallTipWidget(self._control)
103 self._completion_lexer = CompletionLexer(PythonLexer())
104 self._completion_lexer = CompletionLexer(PythonLexer())
104 self._hidden = False
105 self._hidden = False
105 self._highlighter = self._highlighter_class(self)
106 self._highlighter = FrontendHighlighter(self)
106 self._input_splitter = self._input_splitter_class(input_mode='block')
107 self._input_splitter = self._input_splitter_class(input_mode='block')
107 self._kernel_manager = None
108 self._kernel_manager = None
108
109
109 # Configure the ConsoleWidget.
110 # Configure the ConsoleWidget.
110 self.tab_width = 4
111 self.tab_width = 4
111 self._set_continuation_prompt('... ')
112 self._set_continuation_prompt('... ')
112
113
113 # Connect signal handlers.
114 # Connect signal handlers.
114 document = self._control.document()
115 document = self._control.document()
115 document.contentsChange.connect(self._document_contents_change)
116 document.contentsChange.connect(self._document_contents_change)
116
117
117 #---------------------------------------------------------------------------
118 #---------------------------------------------------------------------------
118 # 'ConsoleWidget' abstract interface
119 # 'ConsoleWidget' abstract interface
119 #---------------------------------------------------------------------------
120 #---------------------------------------------------------------------------
120
121
121 def _is_complete(self, source, interactive):
122 def _is_complete(self, source, interactive):
122 """ Returns whether 'source' can be completely processed and a new
123 """ Returns whether 'source' can be completely processed and a new
123 prompt created. When triggered by an Enter/Return key press,
124 prompt created. When triggered by an Enter/Return key press,
124 'interactive' is True; otherwise, it is False.
125 'interactive' is True; otherwise, it is False.
125 """
126 """
126 complete = self._input_splitter.push(source.expandtabs(4))
127 complete = self._input_splitter.push(source.expandtabs(4))
127 if interactive:
128 if interactive:
128 complete = not self._input_splitter.push_accepts_more()
129 complete = not self._input_splitter.push_accepts_more()
129 return complete
130 return complete
130
131
131 def _execute(self, source, hidden):
132 def _execute(self, source, hidden):
132 """ Execute 'source'. If 'hidden', do not show any output.
133 """ Execute 'source'. If 'hidden', do not show any output.
133 """
134 """
134 self.kernel_manager.xreq_channel.execute(source, hidden)
135 self.kernel_manager.xreq_channel.execute(source, hidden)
135 self._hidden = hidden
136 self._hidden = hidden
136
137
137 def _prompt_started_hook(self):
138 def _prompt_started_hook(self):
138 """ Called immediately after a new prompt is displayed.
139 """ Called immediately after a new prompt is displayed.
139 """
140 """
140 if not self._reading:
141 if not self._reading:
141 self._highlighter.highlighting_on = True
142 self._highlighter.highlighting_on = True
142
143
143 def _prompt_finished_hook(self):
144 def _prompt_finished_hook(self):
144 """ Called immediately after a prompt is finished, i.e. when some input
145 """ Called immediately after a prompt is finished, i.e. when some input
145 will be processed and a new prompt displayed.
146 will be processed and a new prompt displayed.
146 """
147 """
147 if not self._reading:
148 if not self._reading:
148 self._highlighter.highlighting_on = False
149 self._highlighter.highlighting_on = False
149
150
150 def _tab_pressed(self):
151 def _tab_pressed(self):
151 """ Called when the tab key is pressed. Returns whether to continue
152 """ Called when the tab key is pressed. Returns whether to continue
152 processing the event.
153 processing the event.
153 """
154 """
154 # Perform tab completion if:
155 # Perform tab completion if:
155 # 1) The cursor is in the input buffer.
156 # 1) The cursor is in the input buffer.
156 # 2) There is a non-whitespace character before the cursor.
157 # 2) There is a non-whitespace character before the cursor.
157 text = self._get_input_buffer_cursor_line()
158 text = self._get_input_buffer_cursor_line()
158 if text is None:
159 if text is None:
159 return False
160 return False
160 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
161 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
161 if complete:
162 if complete:
162 self._complete()
163 self._complete()
163 return not complete
164 return not complete
164
165
165 #---------------------------------------------------------------------------
166 #---------------------------------------------------------------------------
166 # 'ConsoleWidget' protected interface
167 # 'ConsoleWidget' protected interface
167 #---------------------------------------------------------------------------
168 #---------------------------------------------------------------------------
168
169
169 def _event_filter_console_keypress(self, event):
170 def _event_filter_console_keypress(self, event):
170 """ Reimplemented to allow execution interruption.
171 """ Reimplemented to allow execution interruption.
171 """
172 """
172 key = event.key()
173 key = event.key()
173 if self._executing and self._control_key_down(event.modifiers()):
174 if self._executing and self._control_key_down(event.modifiers()):
174 if key == QtCore.Qt.Key_C:
175 if key == QtCore.Qt.Key_C:
175 self._kernel_interrupt()
176 self._kernel_interrupt()
176 return True
177 return True
177 elif key == QtCore.Qt.Key_Period:
178 elif key == QtCore.Qt.Key_Period:
178 self._kernel_restart()
179 self._kernel_restart()
179 return True
180 return True
180 return super(FrontendWidget, self)._event_filter_console_keypress(event)
181 return super(FrontendWidget, self)._event_filter_console_keypress(event)
181
182
182 def _show_continuation_prompt(self):
183 def _show_continuation_prompt(self):
183 """ Reimplemented for auto-indentation.
184 """ Reimplemented for auto-indentation.
184 """
185 """
185 super(FrontendWidget, self)._show_continuation_prompt()
186 super(FrontendWidget, self)._show_continuation_prompt()
186 spaces = self._input_splitter.indent_spaces
187 spaces = self._input_splitter.indent_spaces
187 self._append_plain_text('\t' * (spaces / self.tab_width))
188 self._append_plain_text('\t' * (spaces / self.tab_width))
188 self._append_plain_text(' ' * (spaces % self.tab_width))
189 self._append_plain_text(' ' * (spaces % self.tab_width))
189
190
190 #---------------------------------------------------------------------------
191 #---------------------------------------------------------------------------
191 # 'BaseFrontendMixin' abstract interface
192 # 'BaseFrontendMixin' abstract interface
192 #---------------------------------------------------------------------------
193 #---------------------------------------------------------------------------
193
194
194 def _handle_complete_reply(self, rep):
195 def _handle_complete_reply(self, rep):
195 """ Handle replies for tab completion.
196 """ Handle replies for tab completion.
196 """
197 """
197 cursor = self._get_cursor()
198 cursor = self._get_cursor()
198 if rep['parent_header']['msg_id'] == self._complete_id and \
199 if rep['parent_header']['msg_id'] == self._complete_id and \
199 cursor.position() == self._complete_pos:
200 cursor.position() == self._complete_pos:
200 text = '.'.join(self._get_context())
201 text = '.'.join(self._get_context())
201 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
202 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
202 self._complete_with_items(cursor, rep['content']['matches'])
203 self._complete_with_items(cursor, rep['content']['matches'])
203
204
204 def _handle_execute_reply(self, msg):
205 def _handle_execute_reply(self, msg):
205 """ Handles replies for code execution.
206 """ Handles replies for code execution.
206 """
207 """
207 if not self._hidden:
208 if not self._hidden:
208 # Make sure that all output from the SUB channel has been processed
209 # Make sure that all output from the SUB channel has been processed
209 # before writing a new prompt.
210 # before writing a new prompt.
210 self.kernel_manager.sub_channel.flush()
211 self.kernel_manager.sub_channel.flush()
211
212
212 content = msg['content']
213 content = msg['content']
213 status = content['status']
214 status = content['status']
214 if status == 'ok':
215 if status == 'ok':
215 self._process_execute_ok(msg)
216 self._process_execute_ok(msg)
216 elif status == 'error':
217 elif status == 'error':
217 self._process_execute_error(msg)
218 self._process_execute_error(msg)
218 elif status == 'abort':
219 elif status == 'abort':
219 self._process_execute_abort(msg)
220 self._process_execute_abort(msg)
220
221
221 self._show_interpreter_prompt_for_reply(msg)
222 self._show_interpreter_prompt_for_reply(msg)
222 self.executed.emit(msg)
223 self.executed.emit(msg)
223
224
224 def _handle_input_request(self, msg):
225 def _handle_input_request(self, msg):
225 """ Handle requests for raw_input.
226 """ Handle requests for raw_input.
226 """
227 """
227 if self._hidden:
228 if self._hidden:
228 raise RuntimeError('Request for raw input during hidden execution.')
229 raise RuntimeError('Request for raw input during hidden execution.')
229
230
230 # Make sure that all output from the SUB channel has been processed
231 # Make sure that all output from the SUB channel has been processed
231 # before entering readline mode.
232 # before entering readline mode.
232 self.kernel_manager.sub_channel.flush()
233 self.kernel_manager.sub_channel.flush()
233
234
234 def callback(line):
235 def callback(line):
235 self.kernel_manager.rep_channel.input(line)
236 self.kernel_manager.rep_channel.input(line)
236 self._readline(msg['content']['prompt'], callback=callback)
237 self._readline(msg['content']['prompt'], callback=callback)
237
238
238 def _handle_object_info_reply(self, rep):
239 def _handle_object_info_reply(self, rep):
239 """ Handle replies for call tips.
240 """ Handle replies for call tips.
240 """
241 """
241 cursor = self._get_cursor()
242 cursor = self._get_cursor()
242 if rep['parent_header']['msg_id'] == self._call_tip_id and \
243 if rep['parent_header']['msg_id'] == self._call_tip_id and \
243 cursor.position() == self._call_tip_pos:
244 cursor.position() == self._call_tip_pos:
244 doc = rep['content']['docstring']
245 doc = rep['content']['docstring']
245 if doc:
246 if doc:
246 self._call_tip_widget.show_docstring(doc)
247 self._call_tip_widget.show_docstring(doc)
247
248
248 def _handle_pyout(self, msg):
249 def _handle_pyout(self, msg):
249 """ Handle display hook output.
250 """ Handle display hook output.
250 """
251 """
251 if not self._hidden and self._is_from_this_session(msg):
252 if not self._hidden and self._is_from_this_session(msg):
252 self._append_plain_text(msg['content']['data'] + '\n')
253 self._append_plain_text(msg['content']['data'] + '\n')
253
254
254 def _handle_stream(self, msg):
255 def _handle_stream(self, msg):
255 """ Handle stdout, stderr, and stdin.
256 """ Handle stdout, stderr, and stdin.
256 """
257 """
257 if not self._hidden and self._is_from_this_session(msg):
258 if not self._hidden and self._is_from_this_session(msg):
258 self._append_plain_text(msg['content']['data'])
259 self._append_plain_text(msg['content']['data'])
259 self._control.moveCursor(QtGui.QTextCursor.End)
260 self._control.moveCursor(QtGui.QTextCursor.End)
260
261
261 def _started_channels(self):
262 def _started_channels(self):
262 """ Called when the KernelManager channels have started listening or
263 """ Called when the KernelManager channels have started listening or
263 when the frontend is assigned an already listening KernelManager.
264 when the frontend is assigned an already listening KernelManager.
264 """
265 """
265 self._control.clear()
266 self._control.clear()
266 self._append_plain_text(self._get_banner())
267 self._append_plain_text(self._get_banner())
267 self._show_interpreter_prompt()
268 self._show_interpreter_prompt()
268
269
269 def _stopped_channels(self):
270 def _stopped_channels(self):
270 """ Called when the KernelManager channels have stopped listening or
271 """ Called when the KernelManager channels have stopped listening or
271 when a listening KernelManager is removed from the frontend.
272 when a listening KernelManager is removed from the frontend.
272 """
273 """
273 self._executing = self._reading = False
274 self._executing = self._reading = False
274 self._highlighter.highlighting_on = False
275 self._highlighter.highlighting_on = False
275
276
276 #---------------------------------------------------------------------------
277 #---------------------------------------------------------------------------
277 # 'FrontendWidget' interface
278 # 'FrontendWidget' interface
278 #---------------------------------------------------------------------------
279 #---------------------------------------------------------------------------
279
280
280 def execute_file(self, path, hidden=False):
281 def execute_file(self, path, hidden=False):
281 """ Attempts to execute file with 'path'. If 'hidden', no output is
282 """ Attempts to execute file with 'path'. If 'hidden', no output is
282 shown.
283 shown.
283 """
284 """
284 self.execute('execfile("%s")' % path, hidden=hidden)
285 self.execute('execfile("%s")' % path, hidden=hidden)
285
286
286 #---------------------------------------------------------------------------
287 #---------------------------------------------------------------------------
287 # 'FrontendWidget' protected interface
288 # 'FrontendWidget' protected interface
288 #---------------------------------------------------------------------------
289 #---------------------------------------------------------------------------
289
290
290 def _call_tip(self):
291 def _call_tip(self):
291 """ Shows a call tip, if appropriate, at the current cursor location.
292 """ Shows a call tip, if appropriate, at the current cursor location.
292 """
293 """
293 # Decide if it makes sense to show a call tip
294 # Decide if it makes sense to show a call tip
294 cursor = self._get_cursor()
295 cursor = self._get_cursor()
295 cursor.movePosition(QtGui.QTextCursor.Left)
296 cursor.movePosition(QtGui.QTextCursor.Left)
296 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
297 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
297 return False
298 return False
298 context = self._get_context(cursor)
299 context = self._get_context(cursor)
299 if not context:
300 if not context:
300 return False
301 return False
301
302
302 # Send the metadata request to the kernel
303 # Send the metadata request to the kernel
303 name = '.'.join(context)
304 name = '.'.join(context)
304 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
305 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
305 self._call_tip_pos = self._get_cursor().position()
306 self._call_tip_pos = self._get_cursor().position()
306 return True
307 return True
307
308
308 def _complete(self):
309 def _complete(self):
309 """ Performs completion at the current cursor location.
310 """ Performs completion at the current cursor location.
310 """
311 """
311 context = self._get_context()
312 context = self._get_context()
312 if context:
313 if context:
313 # Send the completion request to the kernel
314 # Send the completion request to the kernel
314 self._complete_id = self.kernel_manager.xreq_channel.complete(
315 self._complete_id = self.kernel_manager.xreq_channel.complete(
315 '.'.join(context), # text
316 '.'.join(context), # text
316 self._get_input_buffer_cursor_line(), # line
317 self._get_input_buffer_cursor_line(), # line
317 self._get_input_buffer_cursor_column(), # cursor_pos
318 self._get_input_buffer_cursor_column(), # cursor_pos
318 self.input_buffer) # block
319 self.input_buffer) # block
319 self._complete_pos = self._get_cursor().position()
320 self._complete_pos = self._get_cursor().position()
320
321
321 def _get_banner(self):
322 def _get_banner(self):
322 """ Gets a banner to display at the beginning of a session.
323 """ Gets a banner to display at the beginning of a session.
323 """
324 """
324 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
325 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
325 '"license" for more information.'
326 '"license" for more information.'
326 return banner % (sys.version, sys.platform)
327 return banner % (sys.version, sys.platform)
327
328
328 def _get_context(self, cursor=None):
329 def _get_context(self, cursor=None):
329 """ Gets the context for the specified cursor (or the current cursor
330 """ Gets the context for the specified cursor (or the current cursor
330 if none is specified).
331 if none is specified).
331 """
332 """
332 if cursor is None:
333 if cursor is None:
333 cursor = self._get_cursor()
334 cursor = self._get_cursor()
334 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
335 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
335 QtGui.QTextCursor.KeepAnchor)
336 QtGui.QTextCursor.KeepAnchor)
336 text = str(cursor.selection().toPlainText())
337 text = str(cursor.selection().toPlainText())
337 return self._completion_lexer.get_context(text)
338 return self._completion_lexer.get_context(text)
338
339
339 def _kernel_interrupt(self):
340 def _kernel_interrupt(self):
340 """ Attempts to interrupt the running kernel.
341 """ Attempts to interrupt the running kernel.
341 """
342 """
342 if self.custom_interrupt:
343 if self.custom_interrupt:
343 self.custom_interrupt_requested.emit()
344 self.custom_interrupt_requested.emit()
344 elif self.kernel_manager.has_kernel:
345 elif self.kernel_manager.has_kernel:
345 self.kernel_manager.signal_kernel(signal.SIGINT)
346 self.kernel_manager.signal_kernel(signal.SIGINT)
346 else:
347 else:
347 self._append_plain_text('Kernel process is either remote or '
348 self._append_plain_text('Kernel process is either remote or '
348 'unspecified. Cannot interrupt.\n')
349 'unspecified. Cannot interrupt.\n')
349
350
350 def _kernel_restart(self):
351 def _kernel_restart(self):
351 """ Attempts to restart the running kernel.
352 """ Attempts to restart the running kernel.
352 """
353 """
353 if self.custom_restart:
354 if self.custom_restart:
354 self.custom_restart_requested.emit()
355 self.custom_restart_requested.emit()
355 elif self.kernel_manager.has_kernel:
356 elif self.kernel_manager.has_kernel:
356 try:
357 try:
357 self.kernel_manager.restart_kernel()
358 self.kernel_manager.restart_kernel()
358 except RuntimeError:
359 except RuntimeError:
359 message = 'Kernel started externally. Cannot restart.\n'
360 message = 'Kernel started externally. Cannot restart.\n'
360 self._append_plain_text(message)
361 self._append_plain_text(message)
361 else:
362 else:
362 self._stopped_channels()
363 self._stopped_channels()
363 self._append_plain_text('Kernel restarting...\n')
364 self._append_plain_text('Kernel restarting...\n')
364 self._show_interpreter_prompt()
365 self._show_interpreter_prompt()
365 else:
366 else:
366 self._append_plain_text('Kernel process is either remote or '
367 self._append_plain_text('Kernel process is either remote or '
367 'unspecified. Cannot restart.\n')
368 'unspecified. Cannot restart.\n')
368
369
369 def _process_execute_abort(self, msg):
370 def _process_execute_abort(self, msg):
370 """ Process a reply for an aborted execution request.
371 """ Process a reply for an aborted execution request.
371 """
372 """
372 self._append_plain_text("ERROR: execution aborted\n")
373 self._append_plain_text("ERROR: execution aborted\n")
373
374
374 def _process_execute_error(self, msg):
375 def _process_execute_error(self, msg):
375 """ Process a reply for an execution request that resulted in an error.
376 """ Process a reply for an execution request that resulted in an error.
376 """
377 """
377 content = msg['content']
378 content = msg['content']
378 traceback = ''.join(content['traceback'])
379 traceback = ''.join(content['traceback'])
379 self._append_plain_text(traceback)
380 self._append_plain_text(traceback)
380
381
381 def _process_execute_ok(self, msg):
382 def _process_execute_ok(self, msg):
382 """ Process a reply for a successful execution equest.
383 """ Process a reply for a successful execution equest.
383 """
384 """
384 payload = msg['content']['payload']
385 payload = msg['content']['payload']
385 for item in payload:
386 for item in payload:
386 if not self._process_execute_payload(item):
387 if not self._process_execute_payload(item):
387 warning = 'Received unknown payload of type %s\n'
388 warning = 'Received unknown payload of type %s\n'
388 self._append_plain_text(warning % repr(item['source']))
389 self._append_plain_text(warning % repr(item['source']))
389
390
390 def _process_execute_payload(self, item):
391 def _process_execute_payload(self, item):
391 """ Process a single payload item from the list of payload items in an
392 """ Process a single payload item from the list of payload items in an
392 execution reply. Returns whether the payload was handled.
393 execution reply. Returns whether the payload was handled.
393 """
394 """
394 # The basic FrontendWidget doesn't handle payloads, as they are a
395 # The basic FrontendWidget doesn't handle payloads, as they are a
395 # mechanism for going beyond the standard Python interpreter model.
396 # mechanism for going beyond the standard Python interpreter model.
396 return False
397 return False
397
398
398 def _show_interpreter_prompt(self):
399 def _show_interpreter_prompt(self):
399 """ Shows a prompt for the interpreter.
400 """ Shows a prompt for the interpreter.
400 """
401 """
401 self._show_prompt('>>> ')
402 self._show_prompt('>>> ')
402
403
403 def _show_interpreter_prompt_for_reply(self, msg):
404 def _show_interpreter_prompt_for_reply(self, msg):
404 """ Shows a prompt for the interpreter given an 'execute_reply' message.
405 """ Shows a prompt for the interpreter given an 'execute_reply' message.
405 """
406 """
406 self._show_interpreter_prompt()
407 self._show_interpreter_prompt()
407
408
408 #------ Signal handlers ----------------------------------------------------
409 #------ Signal handlers ----------------------------------------------------
409
410
410 def _document_contents_change(self, position, removed, added):
411 def _document_contents_change(self, position, removed, added):
411 """ Called whenever the document's content changes. Display a call tip
412 """ Called whenever the document's content changes. Display a call tip
412 if appropriate.
413 if appropriate.
413 """
414 """
414 # Calculate where the cursor should be *after* the change:
415 # Calculate where the cursor should be *after* the change:
415 position += added
416 position += added
416
417
417 document = self._control.document()
418 document = self._control.document()
418 if position == self._get_cursor().position():
419 if position == self._get_cursor().position():
419 self._call_tip()
420 self._call_tip()
@@ -1,230 +1,226 b''
1 # System library imports.
1 # System library imports.
2 from PyQt4 import QtGui
2 from PyQt4 import QtGui
3 from pygments.formatters.html import HtmlFormatter
3 from pygments.formatters.html import HtmlFormatter
4 from pygments.lexer import RegexLexer, _TokenType, Text, Error
4 from pygments.lexer import RegexLexer, _TokenType, Text, Error
5 from pygments.lexers import PythonLexer
5 from pygments.lexers import PythonLexer
6 from pygments.styles import get_style_by_name
6 from pygments.styles import get_style_by_name
7
7
8
8
9 def get_tokens_unprocessed(self, text, stack=('root',)):
9 def get_tokens_unprocessed(self, text, stack=('root',)):
10 """ Split ``text`` into (tokentype, text) pairs.
10 """ Split ``text`` into (tokentype, text) pairs.
11
11
12 Monkeypatched to store the final stack on the object itself.
12 Monkeypatched to store the final stack on the object itself.
13 """
13 """
14 pos = 0
14 pos = 0
15 tokendefs = self._tokens
15 tokendefs = self._tokens
16 if hasattr(self, '_saved_state_stack'):
16 if hasattr(self, '_saved_state_stack'):
17 statestack = list(self._saved_state_stack)
17 statestack = list(self._saved_state_stack)
18 else:
18 else:
19 statestack = list(stack)
19 statestack = list(stack)
20 statetokens = tokendefs[statestack[-1]]
20 statetokens = tokendefs[statestack[-1]]
21 while 1:
21 while 1:
22 for rexmatch, action, new_state in statetokens:
22 for rexmatch, action, new_state in statetokens:
23 m = rexmatch(text, pos)
23 m = rexmatch(text, pos)
24 if m:
24 if m:
25 if type(action) is _TokenType:
25 if type(action) is _TokenType:
26 yield pos, action, m.group()
26 yield pos, action, m.group()
27 else:
27 else:
28 for item in action(self, m):
28 for item in action(self, m):
29 yield item
29 yield item
30 pos = m.end()
30 pos = m.end()
31 if new_state is not None:
31 if new_state is not None:
32 # state transition
32 # state transition
33 if isinstance(new_state, tuple):
33 if isinstance(new_state, tuple):
34 for state in new_state:
34 for state in new_state:
35 if state == '#pop':
35 if state == '#pop':
36 statestack.pop()
36 statestack.pop()
37 elif state == '#push':
37 elif state == '#push':
38 statestack.append(statestack[-1])
38 statestack.append(statestack[-1])
39 else:
39 else:
40 statestack.append(state)
40 statestack.append(state)
41 elif isinstance(new_state, int):
41 elif isinstance(new_state, int):
42 # pop
42 # pop
43 del statestack[new_state:]
43 del statestack[new_state:]
44 elif new_state == '#push':
44 elif new_state == '#push':
45 statestack.append(statestack[-1])
45 statestack.append(statestack[-1])
46 else:
46 else:
47 assert False, "wrong state def: %r" % new_state
47 assert False, "wrong state def: %r" % new_state
48 statetokens = tokendefs[statestack[-1]]
48 statetokens = tokendefs[statestack[-1]]
49 break
49 break
50 else:
50 else:
51 try:
51 try:
52 if text[pos] == '\n':
52 if text[pos] == '\n':
53 # at EOL, reset state to "root"
53 # at EOL, reset state to "root"
54 pos += 1
54 pos += 1
55 statestack = ['root']
55 statestack = ['root']
56 statetokens = tokendefs['root']
56 statetokens = tokendefs['root']
57 yield pos, Text, u'\n'
57 yield pos, Text, u'\n'
58 continue
58 continue
59 yield pos, Error, text[pos]
59 yield pos, Error, text[pos]
60 pos += 1
60 pos += 1
61 except IndexError:
61 except IndexError:
62 break
62 break
63 self._saved_state_stack = list(statestack)
63 self._saved_state_stack = list(statestack)
64
64
65 # Monkeypatch!
65 # Monkeypatch!
66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
67
67
68
68
69 class PygmentsBlockUserData(QtGui.QTextBlockUserData):
69 class PygmentsBlockUserData(QtGui.QTextBlockUserData):
70 """ Storage for the user data associated with each line.
70 """ Storage for the user data associated with each line.
71 """
71 """
72
72
73 syntax_stack = ('root',)
73 syntax_stack = ('root',)
74
74
75 def __init__(self, **kwds):
75 def __init__(self, **kwds):
76 for key, value in kwds.iteritems():
76 for key, value in kwds.iteritems():
77 setattr(self, key, value)
77 setattr(self, key, value)
78 QtGui.QTextBlockUserData.__init__(self)
78 QtGui.QTextBlockUserData.__init__(self)
79
79
80 def __repr__(self):
80 def __repr__(self):
81 attrs = ['syntax_stack']
81 attrs = ['syntax_stack']
82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
83 for attr in attrs ])
83 for attr in attrs ])
84 return 'PygmentsBlockUserData(%s)' % kwds
84 return 'PygmentsBlockUserData(%s)' % kwds
85
85
86
86
87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
88 """ Syntax highlighter that uses Pygments for parsing. """
88 """ Syntax highlighter that uses Pygments for parsing. """
89
89
90 #---------------------------------------------------------------------------
90 #---------------------------------------------------------------------------
91 # 'QSyntaxHighlighter' interface
91 # 'QSyntaxHighlighter' interface
92 #---------------------------------------------------------------------------
92 #---------------------------------------------------------------------------
93
93
94 def __init__(self, parent, lexer=None):
94 def __init__(self, parent, lexer=None):
95 super(PygmentsHighlighter, self).__init__(parent)
95 super(PygmentsHighlighter, self).__init__(parent)
96
96
97 self._document = QtGui.QTextDocument()
97 self._document = QtGui.QTextDocument()
98 self._formatter = HtmlFormatter(nowrap=True)
98 self._formatter = HtmlFormatter(nowrap=True)
99 self._lexer = lexer if lexer else PythonLexer()
99 self._lexer = lexer if lexer else PythonLexer()
100 self.set_style('default')
100 self.set_style('default')
101
101
102 def highlightBlock(self, qstring):
102 def highlightBlock(self, qstring):
103 """ Highlight a block of text.
103 """ Highlight a block of text.
104 """
104 """
105 qstring = unicode(qstring)
105 qstring = unicode(qstring)
106 prev_data = self.currentBlock().previous().userData()
106 prev_data = self.currentBlock().previous().userData()
107
107
108 if prev_data is not None:
108 if prev_data is not None:
109 self._lexer._saved_state_stack = prev_data.syntax_stack
109 self._lexer._saved_state_stack = prev_data.syntax_stack
110 elif hasattr(self._lexer, '_saved_state_stack'):
110 elif hasattr(self._lexer, '_saved_state_stack'):
111 del self._lexer._saved_state_stack
111 del self._lexer._saved_state_stack
112
112
113 index = 0
114 # Lex the text using Pygments
113 # Lex the text using Pygments
114 index = 0
115 for token, text in self._lexer.get_tokens(qstring):
115 for token, text in self._lexer.get_tokens(qstring):
116 l = len(text)
116 length = len(text)
117 format = self._get_format(token)
117 self.setFormat(index, length, self._get_format(token))
118 if format is not None:
118 index += length
119 self.setFormat(index, l, format)
120 index += l
121
119
122 if hasattr(self._lexer, '_saved_state_stack'):
120 if hasattr(self._lexer, '_saved_state_stack'):
123 data = PygmentsBlockUserData(
121 data = PygmentsBlockUserData(
124 syntax_stack=self._lexer._saved_state_stack)
122 syntax_stack=self._lexer._saved_state_stack)
125 self.currentBlock().setUserData(data)
123 self.currentBlock().setUserData(data)
126 # Clean up for the next go-round.
124 # Clean up for the next go-round.
127 del self._lexer._saved_state_stack
125 del self._lexer._saved_state_stack
128
126
129 #---------------------------------------------------------------------------
127 #---------------------------------------------------------------------------
130 # 'PygmentsHighlighter' interface
128 # 'PygmentsHighlighter' interface
131 #---------------------------------------------------------------------------
129 #---------------------------------------------------------------------------
132
130
133 def set_style(self, style):
131 def set_style(self, style):
134 """ Sets the style to the specified Pygments style.
132 """ Sets the style to the specified Pygments style.
135 """
133 """
136 if isinstance(style, basestring):
134 if isinstance(style, basestring):
137 style = get_style_by_name(style)
135 style = get_style_by_name(style)
138 self._style = style
136 self._style = style
139 self._clear_caches()
137 self._clear_caches()
140
138
141 def set_style_sheet(self, stylesheet):
139 def set_style_sheet(self, stylesheet):
142 """ Sets a CSS stylesheet. The classes in the stylesheet should
140 """ Sets a CSS stylesheet. The classes in the stylesheet should
143 correspond to those generated by:
141 correspond to those generated by:
144
142
145 pygmentize -S <style> -f html
143 pygmentize -S <style> -f html
146
144
147 Note that 'set_style' and 'set_style_sheet' completely override each
145 Note that 'set_style' and 'set_style_sheet' completely override each
148 other, i.e. they cannot be used in conjunction.
146 other, i.e. they cannot be used in conjunction.
149 """
147 """
150 self._document.setDefaultStyleSheet(stylesheet)
148 self._document.setDefaultStyleSheet(stylesheet)
151 self._style = None
149 self._style = None
152 self._clear_caches()
150 self._clear_caches()
153
151
154 #---------------------------------------------------------------------------
152 #---------------------------------------------------------------------------
155 # Protected interface
153 # Protected interface
156 #---------------------------------------------------------------------------
154 #---------------------------------------------------------------------------
157
155
158 def _clear_caches(self):
156 def _clear_caches(self):
159 """ Clear caches for brushes and formats.
157 """ Clear caches for brushes and formats.
160 """
158 """
161 self._brushes = {}
159 self._brushes = {}
162 self._formats = {}
160 self._formats = {}
163
161
164 def _get_format(self, token):
162 def _get_format(self, token):
165 """ Returns a QTextCharFormat for token or None.
163 """ Returns a QTextCharFormat for token or None.
166 """
164 """
167 if token in self._formats:
165 if token in self._formats:
168 return self._formats[token]
166 return self._formats[token]
169
167
170 if self._style is None:
168 if self._style is None:
171 result = self._get_format_from_document(token, self._document)
169 result = self._get_format_from_document(token, self._document)
172 else:
170 else:
173 result = self._get_format_from_style(token, self._style)
171 result = self._get_format_from_style(token, self._style)
174
172
175 self._formats[token] = result
173 self._formats[token] = result
176 return result
174 return result
177
175
178 def _get_format_from_document(self, token, document):
176 def _get_format_from_document(self, token, document):
179 """ Returns a QTextCharFormat for token by
177 """ Returns a QTextCharFormat for token by
180 """
178 """
181 code, html = self._formatter._format_lines([(token, 'dummy')]).next()
179 code, html = self._formatter._format_lines([(token, 'dummy')]).next()
182 self._document.setHtml(html)
180 self._document.setHtml(html)
183 return QtGui.QTextCursor(self._document).charFormat()
181 return QtGui.QTextCursor(self._document).charFormat()
184
182
185 def _get_format_from_style(self, token, style):
183 def _get_format_from_style(self, token, style):
186 """ Returns a QTextCharFormat for token by reading a Pygments style.
184 """ Returns a QTextCharFormat for token by reading a Pygments style.
187 """
185 """
188 result = None
186 result = QtGui.QTextCharFormat()
189 for key, value in style.style_for_token(token).items():
187 for key, value in style.style_for_token(token).items():
190 if value:
188 if value:
191 if result is None:
192 result = QtGui.QTextCharFormat()
193 if key == 'color':
189 if key == 'color':
194 result.setForeground(self._get_brush(value))
190 result.setForeground(self._get_brush(value))
195 elif key == 'bgcolor':
191 elif key == 'bgcolor':
196 result.setBackground(self._get_brush(value))
192 result.setBackground(self._get_brush(value))
197 elif key == 'bold':
193 elif key == 'bold':
198 result.setFontWeight(QtGui.QFont.Bold)
194 result.setFontWeight(QtGui.QFont.Bold)
199 elif key == 'italic':
195 elif key == 'italic':
200 result.setFontItalic(True)
196 result.setFontItalic(True)
201 elif key == 'underline':
197 elif key == 'underline':
202 result.setUnderlineStyle(
198 result.setUnderlineStyle(
203 QtGui.QTextCharFormat.SingleUnderline)
199 QtGui.QTextCharFormat.SingleUnderline)
204 elif key == 'sans':
200 elif key == 'sans':
205 result.setFontStyleHint(QtGui.QFont.SansSerif)
201 result.setFontStyleHint(QtGui.QFont.SansSerif)
206 elif key == 'roman':
202 elif key == 'roman':
207 result.setFontStyleHint(QtGui.QFont.Times)
203 result.setFontStyleHint(QtGui.QFont.Times)
208 elif key == 'mono':
204 elif key == 'mono':
209 result.setFontStyleHint(QtGui.QFont.TypeWriter)
205 result.setFontStyleHint(QtGui.QFont.TypeWriter)
210 return result
206 return result
211
207
212 def _get_brush(self, color):
208 def _get_brush(self, color):
213 """ Returns a brush for the color.
209 """ Returns a brush for the color.
214 """
210 """
215 result = self._brushes.get(color)
211 result = self._brushes.get(color)
216 if result is None:
212 if result is None:
217 qcolor = self._get_color(color)
213 qcolor = self._get_color(color)
218 result = QtGui.QBrush(qcolor)
214 result = QtGui.QBrush(qcolor)
219 self._brushes[color] = result
215 self._brushes[color] = result
220 return result
216 return result
221
217
222 def _get_color(self, color):
218 def _get_color(self, color):
223 """ Returns a QColor built from a Pygments color string.
219 """ Returns a QColor built from a Pygments color string.
224 """
220 """
225 qcolor = QtGui.QColor()
221 qcolor = QtGui.QColor()
226 qcolor.setRgb(int(color[:2], base=16),
222 qcolor.setRgb(int(color[:2], base=16),
227 int(color[2:4], base=16),
223 int(color[2:4], base=16),
228 int(color[4:6], base=16))
224 int(color[4:6], base=16))
229 return qcolor
225 return qcolor
230
226
General Comments 0
You need to be logged in to leave comments. Login now