Show More
@@ -1,383 +1,380 b'' | |||||
1 | # Standard library imports |
|
1 | # Standard library imports | |
2 | from codeop import CommandCompiler |
|
2 | from codeop import CommandCompiler | |
3 | from threading import Thread |
|
3 | from threading import Thread | |
4 | import time |
|
4 | import time | |
5 | import types |
|
5 | import types | |
6 |
|
6 | |||
7 | # System library imports |
|
7 | # System library imports | |
8 | from IPython.zmq.session import Message, Session |
|
8 | from IPython.zmq.session import Message, Session | |
9 | from pygments.lexers import PythonLexer |
|
9 | from pygments.lexers import PythonLexer | |
10 | from PyQt4 import QtCore, QtGui |
|
10 | from PyQt4 import QtCore, QtGui | |
11 | import zmq |
|
11 | import zmq | |
12 |
|
12 | |||
13 | # ETS imports |
|
|||
14 | from enthought.pyface.ui.qt4.code_editor.pygments_highlighter import \ |
|
|||
15 | PygmentsHighlighter |
|
|||
16 |
|
||||
17 | # Local imports |
|
13 | # Local imports | |
18 | from call_tip_widget import CallTipWidget |
|
14 | from call_tip_widget import CallTipWidget | |
19 | from completion_lexer import CompletionLexer |
|
15 | from completion_lexer import CompletionLexer | |
20 | from console_widget import HistoryConsoleWidget |
|
16 | from console_widget import HistoryConsoleWidget | |
|
17 | from pygments_highlighter import PygmentsHighlighter | |||
21 |
|
18 | |||
22 |
|
19 | |||
23 | class FrontendReplyThread(Thread, QtCore.QObject): |
|
20 | class FrontendReplyThread(Thread, QtCore.QObject): | |
24 | """ A Thread that receives a reply from the kernel for the frontend. |
|
21 | """ A Thread that receives a reply from the kernel for the frontend. | |
25 | """ |
|
22 | """ | |
26 |
|
23 | |||
27 | finished = QtCore.pyqtSignal() |
|
24 | finished = QtCore.pyqtSignal() | |
28 | output_received = QtCore.pyqtSignal(Message) |
|
25 | output_received = QtCore.pyqtSignal(Message) | |
29 | reply_received = QtCore.pyqtSignal(Message) |
|
26 | reply_received = QtCore.pyqtSignal(Message) | |
30 |
|
27 | |||
31 | def __init__(self, parent): |
|
28 | def __init__(self, parent): | |
32 | """ Create a FrontendReplyThread for the specified frontend. |
|
29 | """ Create a FrontendReplyThread for the specified frontend. | |
33 | """ |
|
30 | """ | |
34 | assert isinstance(parent, FrontendWidget) |
|
31 | assert isinstance(parent, FrontendWidget) | |
35 | QtCore.QObject.__init__(self, parent) |
|
32 | QtCore.QObject.__init__(self, parent) | |
36 | Thread.__init__(self) |
|
33 | Thread.__init__(self) | |
37 |
|
34 | |||
38 | self.sleep_time = 0.05 |
|
35 | self.sleep_time = 0.05 | |
39 |
|
36 | |||
40 | def run(self): |
|
37 | def run(self): | |
41 | """ The starting point for the thread. |
|
38 | """ The starting point for the thread. | |
42 | """ |
|
39 | """ | |
43 | frontend = self.parent() |
|
40 | frontend = self.parent() | |
44 | while True: |
|
41 | while True: | |
45 | rep = frontend._recv_reply() |
|
42 | rep = frontend._recv_reply() | |
46 | if rep is not None: |
|
43 | if rep is not None: | |
47 | self._recv_output() |
|
44 | self._recv_output() | |
48 | self.reply_received.emit(rep) |
|
45 | self.reply_received.emit(rep) | |
49 | break |
|
46 | break | |
50 |
|
47 | |||
51 | self._recv_output() |
|
48 | self._recv_output() | |
52 | time.sleep(self.sleep_time) |
|
49 | time.sleep(self.sleep_time) | |
53 |
|
50 | |||
54 | self.finished.emit() |
|
51 | self.finished.emit() | |
55 |
|
52 | |||
56 | def _recv_output(self): |
|
53 | def _recv_output(self): | |
57 | """ Send any output to the frontend. |
|
54 | """ Send any output to the frontend. | |
58 | """ |
|
55 | """ | |
59 | frontend = self.parent() |
|
56 | frontend = self.parent() | |
60 | omsgs = frontend._recv_output() |
|
57 | omsgs = frontend._recv_output() | |
61 | for omsg in omsgs: |
|
58 | for omsg in omsgs: | |
62 | self.output_received.emit(omsg) |
|
59 | self.output_received.emit(omsg) | |
63 |
|
60 | |||
64 |
|
61 | |||
65 | class FrontendHighlighter(PygmentsHighlighter): |
|
62 | class FrontendHighlighter(PygmentsHighlighter): | |
66 | """ A Python PygmentsHighlighter that can be turned on and off and which |
|
63 | """ A Python PygmentsHighlighter that can be turned on and off and which | |
67 | knows about continuation prompts. |
|
64 | knows about continuation prompts. | |
68 | """ |
|
65 | """ | |
69 |
|
66 | |||
70 | def __init__(self, frontend): |
|
67 | def __init__(self, frontend): | |
71 | PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer()) |
|
68 | PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer()) | |
72 | self._current_offset = 0 |
|
69 | self._current_offset = 0 | |
73 | self._frontend = frontend |
|
70 | self._frontend = frontend | |
74 | self.highlighting_on = False |
|
71 | self.highlighting_on = False | |
75 |
|
72 | |||
76 | def highlightBlock(self, qstring): |
|
73 | def highlightBlock(self, qstring): | |
77 | """ Highlight a block of text. Reimplemented to highlight selectively. |
|
74 | """ Highlight a block of text. Reimplemented to highlight selectively. | |
78 | """ |
|
75 | """ | |
79 | if self.highlighting_on: |
|
76 | if self.highlighting_on: | |
80 | for prompt in (self._frontend._prompt, |
|
77 | for prompt in (self._frontend._prompt, | |
81 | self._frontend.continuation_prompt): |
|
78 | self._frontend.continuation_prompt): | |
82 | if qstring.startsWith(prompt): |
|
79 | if qstring.startsWith(prompt): | |
83 | qstring.remove(0, len(prompt)) |
|
80 | qstring.remove(0, len(prompt)) | |
84 | self._current_offset = len(prompt) |
|
81 | self._current_offset = len(prompt) | |
85 | break |
|
82 | break | |
86 | PygmentsHighlighter.highlightBlock(self, qstring) |
|
83 | PygmentsHighlighter.highlightBlock(self, qstring) | |
87 |
|
84 | |||
88 | def setFormat(self, start, count, format): |
|
85 | def setFormat(self, start, count, format): | |
89 | """ Reimplemented to avoid highlighting continuation prompts. |
|
86 | """ Reimplemented to avoid highlighting continuation prompts. | |
90 | """ |
|
87 | """ | |
91 | start += self._current_offset |
|
88 | start += self._current_offset | |
92 | PygmentsHighlighter.setFormat(self, start, count, format) |
|
89 | PygmentsHighlighter.setFormat(self, start, count, format) | |
93 |
|
90 | |||
94 |
|
91 | |||
95 | class FrontendWidget(HistoryConsoleWidget): |
|
92 | class FrontendWidget(HistoryConsoleWidget): | |
96 | """ A Qt frontend for an IPython kernel. |
|
93 | """ A Qt frontend for an IPython kernel. | |
97 | """ |
|
94 | """ | |
98 |
|
95 | |||
99 | # Emitted when an 'execute_reply' is received from the kernel. |
|
96 | # Emitted when an 'execute_reply' is received from the kernel. | |
100 | executed = QtCore.pyqtSignal(Message) |
|
97 | executed = QtCore.pyqtSignal(Message) | |
101 |
|
98 | |||
102 | #--------------------------------------------------------------------------- |
|
99 | #--------------------------------------------------------------------------- | |
103 | # 'QWidget' interface |
|
100 | # 'QWidget' interface | |
104 | #--------------------------------------------------------------------------- |
|
101 | #--------------------------------------------------------------------------- | |
105 |
|
102 | |||
106 | def __init__(self, parent=None, session=None, request_socket=None, |
|
103 | def __init__(self, parent=None, session=None, request_socket=None, | |
107 | sub_socket=None): |
|
104 | sub_socket=None): | |
108 | super(FrontendWidget, self).__init__(parent) |
|
105 | super(FrontendWidget, self).__init__(parent) | |
109 | self.continuation_prompt = '... ' |
|
106 | self.continuation_prompt = '... ' | |
110 |
|
107 | |||
111 | self._call_tip_widget = CallTipWidget(self) |
|
108 | self._call_tip_widget = CallTipWidget(self) | |
112 | self._compile = CommandCompiler() |
|
109 | self._compile = CommandCompiler() | |
113 | self._completion_lexer = CompletionLexer(PythonLexer()) |
|
110 | self._completion_lexer = CompletionLexer(PythonLexer()) | |
114 | self._highlighter = FrontendHighlighter(self) |
|
111 | self._highlighter = FrontendHighlighter(self) | |
115 |
|
112 | |||
116 | self.session = Session() if session is None else session |
|
113 | self.session = Session() if session is None else session | |
117 | self.request_socket = request_socket |
|
114 | self.request_socket = request_socket | |
118 | self.sub_socket = sub_socket |
|
115 | self.sub_socket = sub_socket | |
119 |
|
116 | |||
120 | self.document().contentsChange.connect(self._document_contents_change) |
|
117 | self.document().contentsChange.connect(self._document_contents_change) | |
121 |
|
118 | |||
122 | self._kernel_connected() # XXX |
|
119 | self._kernel_connected() # XXX | |
123 |
|
120 | |||
124 | def focusOutEvent(self, event): |
|
121 | def focusOutEvent(self, event): | |
125 | """ Reimplemented to hide calltips. |
|
122 | """ Reimplemented to hide calltips. | |
126 | """ |
|
123 | """ | |
127 | self._call_tip_widget.hide() |
|
124 | self._call_tip_widget.hide() | |
128 | return super(FrontendWidget, self).focusOutEvent(event) |
|
125 | return super(FrontendWidget, self).focusOutEvent(event) | |
129 |
|
126 | |||
130 | def keyPressEvent(self, event): |
|
127 | def keyPressEvent(self, event): | |
131 | """ Reimplemented to hide calltips. |
|
128 | """ Reimplemented to hide calltips. | |
132 | """ |
|
129 | """ | |
133 | if event.key() == QtCore.Qt.Key_Escape: |
|
130 | if event.key() == QtCore.Qt.Key_Escape: | |
134 | self._call_tip_widget.hide() |
|
131 | self._call_tip_widget.hide() | |
135 | return super(FrontendWidget, self).keyPressEvent(event) |
|
132 | return super(FrontendWidget, self).keyPressEvent(event) | |
136 |
|
133 | |||
137 | #--------------------------------------------------------------------------- |
|
134 | #--------------------------------------------------------------------------- | |
138 | # 'ConsoleWidget' abstract interface |
|
135 | # 'ConsoleWidget' abstract interface | |
139 | #--------------------------------------------------------------------------- |
|
136 | #--------------------------------------------------------------------------- | |
140 |
|
137 | |||
141 | def _execute(self, interactive): |
|
138 | def _execute(self, interactive): | |
142 | """ Called to execute the input buffer. When triggered by an the enter |
|
139 | """ Called to execute the input buffer. When triggered by an the enter | |
143 | key press, 'interactive' is True; otherwise, it is False. Returns |
|
140 | key press, 'interactive' is True; otherwise, it is False. Returns | |
144 | whether the input buffer was completely processed and a new prompt |
|
141 | whether the input buffer was completely processed and a new prompt | |
145 | created. |
|
142 | created. | |
146 | """ |
|
143 | """ | |
147 | return self.execute_source(self.input_buffer, interactive=interactive) |
|
144 | return self.execute_source(self.input_buffer, interactive=interactive) | |
148 |
|
145 | |||
149 | def _prompt_started_hook(self): |
|
146 | def _prompt_started_hook(self): | |
150 | """ Called immediately after a new prompt is displayed. |
|
147 | """ Called immediately after a new prompt is displayed. | |
151 | """ |
|
148 | """ | |
152 | self._highlighter.highlighting_on = True |
|
149 | self._highlighter.highlighting_on = True | |
153 |
|
150 | |||
154 | def _prompt_finished_hook(self): |
|
151 | def _prompt_finished_hook(self): | |
155 | """ Called immediately after a prompt is finished, i.e. when some input |
|
152 | """ Called immediately after a prompt is finished, i.e. when some input | |
156 | will be processed and a new prompt displayed. |
|
153 | will be processed and a new prompt displayed. | |
157 | """ |
|
154 | """ | |
158 | self._highlighter.highlighting_on = False |
|
155 | self._highlighter.highlighting_on = False | |
159 |
|
156 | |||
160 | def _tab_pressed(self): |
|
157 | def _tab_pressed(self): | |
161 | """ Called when the tab key is pressed. Returns whether to continue |
|
158 | """ Called when the tab key is pressed. Returns whether to continue | |
162 | processing the event. |
|
159 | processing the event. | |
163 | """ |
|
160 | """ | |
164 | self._keep_cursor_in_buffer() |
|
161 | self._keep_cursor_in_buffer() | |
165 | cursor = self.textCursor() |
|
162 | cursor = self.textCursor() | |
166 | if not self._complete(): |
|
163 | if not self._complete(): | |
167 | cursor.insertText(' ') |
|
164 | cursor.insertText(' ') | |
168 | return False |
|
165 | return False | |
169 |
|
166 | |||
170 | #--------------------------------------------------------------------------- |
|
167 | #--------------------------------------------------------------------------- | |
171 | # 'FrontendWidget' interface |
|
168 | # 'FrontendWidget' interface | |
172 | #--------------------------------------------------------------------------- |
|
169 | #--------------------------------------------------------------------------- | |
173 |
|
170 | |||
174 | def execute_source(self, source, hidden=False, interactive=False): |
|
171 | def execute_source(self, source, hidden=False, interactive=False): | |
175 | """ Execute a string containing Python code. If 'hidden', no output is |
|
172 | """ Execute a string containing Python code. If 'hidden', no output is | |
176 | shown. Returns whether the source executed (i.e., returns True only |
|
173 | shown. Returns whether the source executed (i.e., returns True only | |
177 | if no more input is necessary). |
|
174 | if no more input is necessary). | |
178 | """ |
|
175 | """ | |
179 | try: |
|
176 | try: | |
180 | code = self._compile(source, symbol='single') |
|
177 | code = self._compile(source, symbol='single') | |
181 | except (OverflowError, SyntaxError, ValueError): |
|
178 | except (OverflowError, SyntaxError, ValueError): | |
182 | # Just let IPython deal with the syntax error. |
|
179 | # Just let IPython deal with the syntax error. | |
183 | code = Exception |
|
180 | code = Exception | |
184 |
|
181 | |||
185 | # Only execute interactive multiline input if it ends with a blank line |
|
182 | # Only execute interactive multiline input if it ends with a blank line | |
186 | lines = source.splitlines() |
|
183 | lines = source.splitlines() | |
187 | if interactive and len(lines) > 1 and lines[-1].strip() != '': |
|
184 | if interactive and len(lines) > 1 and lines[-1].strip() != '': | |
188 | code = None |
|
185 | code = None | |
189 |
|
186 | |||
190 | executed = code is not None |
|
187 | executed = code is not None | |
191 | if executed: |
|
188 | if executed: | |
192 | msg = self.session.send(self.request_socket, 'execute_request', |
|
189 | msg = self.session.send(self.request_socket, 'execute_request', | |
193 | dict(code=source)) |
|
190 | dict(code=source)) | |
194 | thread = FrontendReplyThread(self) |
|
191 | thread = FrontendReplyThread(self) | |
195 | if not hidden: |
|
192 | if not hidden: | |
196 | thread.output_received.connect(self._handle_output) |
|
193 | thread.output_received.connect(self._handle_output) | |
197 | thread.reply_received.connect(self._handle_reply) |
|
194 | thread.reply_received.connect(self._handle_reply) | |
198 | thread.finished.connect(thread.deleteLater) |
|
195 | thread.finished.connect(thread.deleteLater) | |
199 | thread.start() |
|
196 | thread.start() | |
200 | else: |
|
197 | else: | |
201 | space = 0 |
|
198 | space = 0 | |
202 | for char in lines[-1]: |
|
199 | for char in lines[-1]: | |
203 | if char == '\t': |
|
200 | if char == '\t': | |
204 | space += 4 |
|
201 | space += 4 | |
205 | elif char == ' ': |
|
202 | elif char == ' ': | |
206 | space += 1 |
|
203 | space += 1 | |
207 | else: |
|
204 | else: | |
208 | break |
|
205 | break | |
209 | if source.endswith(':') or source.endswith(':\n'): |
|
206 | if source.endswith(':') or source.endswith(':\n'): | |
210 | space += 4 |
|
207 | space += 4 | |
211 | self._show_continuation_prompt() |
|
208 | self._show_continuation_prompt() | |
212 | self.appendPlainText(' ' * space) |
|
209 | self.appendPlainText(' ' * space) | |
213 |
|
210 | |||
214 | return executed |
|
211 | return executed | |
215 |
|
212 | |||
216 | def execute_file(self, path, hidden=False): |
|
213 | def execute_file(self, path, hidden=False): | |
217 | """ Attempts to execute file with 'path'. If 'hidden', no output is |
|
214 | """ Attempts to execute file with 'path'. If 'hidden', no output is | |
218 | shown. |
|
215 | shown. | |
219 | """ |
|
216 | """ | |
220 | self.execute_source('run %s' % path, hidden=hidden) |
|
217 | self.execute_source('run %s' % path, hidden=hidden) | |
221 |
|
218 | |||
222 | #--------------------------------------------------------------------------- |
|
219 | #--------------------------------------------------------------------------- | |
223 | # 'FrontendWidget' protected interface |
|
220 | # 'FrontendWidget' protected interface | |
224 | #--------------------------------------------------------------------------- |
|
221 | #--------------------------------------------------------------------------- | |
225 |
|
222 | |||
226 | def _call_tip(self): |
|
223 | def _call_tip(self): | |
227 | """ Shows a call tip, if appropriate, at the current cursor location. |
|
224 | """ Shows a call tip, if appropriate, at the current cursor location. | |
228 | """ |
|
225 | """ | |
229 | # Decide if it makes sense to show a call tip |
|
226 | # Decide if it makes sense to show a call tip | |
230 | cursor = self.textCursor() |
|
227 | cursor = self.textCursor() | |
231 | cursor.movePosition(QtGui.QTextCursor.Left) |
|
228 | cursor.movePosition(QtGui.QTextCursor.Left) | |
232 | document = self.document() |
|
229 | document = self.document() | |
233 | if document.characterAt(cursor.position()).toAscii() != '(': |
|
230 | if document.characterAt(cursor.position()).toAscii() != '(': | |
234 | return False |
|
231 | return False | |
235 | context = self._get_context(cursor) |
|
232 | context = self._get_context(cursor) | |
236 | if not context: |
|
233 | if not context: | |
237 | return False |
|
234 | return False | |
238 |
|
235 | |||
239 | # Send the metadata request to the kernel |
|
236 | # Send the metadata request to the kernel | |
240 | text = '.'.join(context) |
|
237 | text = '.'.join(context) | |
241 | msg = self.session.send(self.request_socket, 'metadata_request', |
|
238 | msg = self.session.send(self.request_socket, 'metadata_request', | |
242 | dict(context=text)) |
|
239 | dict(context=text)) | |
243 |
|
240 | |||
244 | # Give the kernel some time to respond |
|
241 | # Give the kernel some time to respond | |
245 | rep = self._recv_reply_now('metadata_reply') |
|
242 | rep = self._recv_reply_now('metadata_reply') | |
246 | doc = rep.content.docstring if rep else '' |
|
243 | doc = rep.content.docstring if rep else '' | |
247 |
|
244 | |||
248 | # Show the call tip |
|
245 | # Show the call tip | |
249 | if doc: |
|
246 | if doc: | |
250 | self._call_tip_widget.show_tip(doc) |
|
247 | self._call_tip_widget.show_tip(doc) | |
251 | return True |
|
248 | return True | |
252 |
|
249 | |||
253 | def _complete(self): |
|
250 | def _complete(self): | |
254 | """ Performs completion at the current cursor location. |
|
251 | """ Performs completion at the current cursor location. | |
255 | """ |
|
252 | """ | |
256 | # Decide if it makes sense to do completion |
|
253 | # Decide if it makes sense to do completion | |
257 | context = self._get_context() |
|
254 | context = self._get_context() | |
258 | if not context: |
|
255 | if not context: | |
259 | return False |
|
256 | return False | |
260 |
|
257 | |||
261 | # Send the completion request to the kernel |
|
258 | # Send the completion request to the kernel | |
262 | text = '.'.join(context) |
|
259 | text = '.'.join(context) | |
263 | line = self.input_buffer_cursor_line |
|
260 | line = self.input_buffer_cursor_line | |
264 | msg = self.session.send(self.request_socket, 'complete_request', |
|
261 | msg = self.session.send(self.request_socket, 'complete_request', | |
265 | dict(text=text, line=line)) |
|
262 | dict(text=text, line=line)) | |
266 |
|
263 | |||
267 | # Give the kernel some time to respond |
|
264 | # Give the kernel some time to respond | |
268 | rep = self._recv_reply_now('complete_reply') |
|
265 | rep = self._recv_reply_now('complete_reply') | |
269 | matches = rep.content.matches if rep else [] |
|
266 | matches = rep.content.matches if rep else [] | |
270 |
|
267 | |||
271 | # Show the completion at the correct location |
|
268 | # Show the completion at the correct location | |
272 | cursor = self.textCursor() |
|
269 | cursor = self.textCursor() | |
273 | cursor.movePosition(QtGui.QTextCursor.Left, n=len(text)) |
|
270 | cursor.movePosition(QtGui.QTextCursor.Left, n=len(text)) | |
274 | self._complete_with_items(cursor, matches) |
|
271 | self._complete_with_items(cursor, matches) | |
275 | return True |
|
272 | return True | |
276 |
|
273 | |||
277 | def _kernel_connected(self): |
|
274 | def _kernel_connected(self): | |
278 | """ Called when the frontend is connected to a kernel. |
|
275 | """ Called when the frontend is connected to a kernel. | |
279 | """ |
|
276 | """ | |
280 | self._show_prompt('>>> ') |
|
277 | self._show_prompt('>>> ') | |
281 |
|
278 | |||
282 | def _get_context(self, cursor=None): |
|
279 | def _get_context(self, cursor=None): | |
283 | """ Gets the context at the current cursor location. |
|
280 | """ Gets the context at the current cursor location. | |
284 | """ |
|
281 | """ | |
285 | if cursor is None: |
|
282 | if cursor is None: | |
286 | cursor = self.textCursor() |
|
283 | cursor = self.textCursor() | |
287 | cursor.movePosition(QtGui.QTextCursor.StartOfLine, |
|
284 | cursor.movePosition(QtGui.QTextCursor.StartOfLine, | |
288 | QtGui.QTextCursor.KeepAnchor) |
|
285 | QtGui.QTextCursor.KeepAnchor) | |
289 | text = unicode(cursor.selectedText()) |
|
286 | text = unicode(cursor.selectedText()) | |
290 | return self._completion_lexer.get_context(text) |
|
287 | return self._completion_lexer.get_context(text) | |
291 |
|
288 | |||
292 | #------ Signal handlers ---------------------------------------------------- |
|
289 | #------ Signal handlers ---------------------------------------------------- | |
293 |
|
290 | |||
294 | def _document_contents_change(self, position, removed, added): |
|
291 | def _document_contents_change(self, position, removed, added): | |
295 | """ Called whenever the document's content changes. Display a calltip |
|
292 | """ Called whenever the document's content changes. Display a calltip | |
296 | if appropriate. |
|
293 | if appropriate. | |
297 | """ |
|
294 | """ | |
298 | # Calculate where the cursor should be *after* the change: |
|
295 | # Calculate where the cursor should be *after* the change: | |
299 | position += added |
|
296 | position += added | |
300 |
|
297 | |||
301 | document = self.document() |
|
298 | document = self.document() | |
302 | if position == self.textCursor().position(): |
|
299 | if position == self.textCursor().position(): | |
303 | self._call_tip() |
|
300 | self._call_tip() | |
304 |
|
301 | |||
305 | def _handle_output(self, omsg): |
|
302 | def _handle_output(self, omsg): | |
306 | handler = getattr(self, '_handle_%s' % omsg.msg_type, None) |
|
303 | handler = getattr(self, '_handle_%s' % omsg.msg_type, None) | |
307 | if handler is not None: |
|
304 | if handler is not None: | |
308 | handler(omsg) |
|
305 | handler(omsg) | |
309 |
|
306 | |||
310 | def _handle_pyout(self, omsg): |
|
307 | def _handle_pyout(self, omsg): | |
311 | if omsg.parent_header.session == self.session.session: |
|
308 | if omsg.parent_header.session == self.session.session: | |
312 | self.appendPlainText(omsg.content.data + '\n') |
|
309 | self.appendPlainText(omsg.content.data + '\n') | |
313 |
|
310 | |||
314 | def _handle_stream(self, omsg): |
|
311 | def _handle_stream(self, omsg): | |
315 | self.appendPlainText(omsg.content.data) |
|
312 | self.appendPlainText(omsg.content.data) | |
316 |
|
313 | |||
317 | def _handle_reply(self, rep): |
|
314 | def _handle_reply(self, rep): | |
318 | if rep is not None: |
|
315 | if rep is not None: | |
319 | if rep.msg_type == 'execute_reply': |
|
316 | if rep.msg_type == 'execute_reply': | |
320 | if rep.content.status == 'error': |
|
317 | if rep.content.status == 'error': | |
321 | self.appendPlainText(rep.content.traceback[-1]) |
|
318 | self.appendPlainText(rep.content.traceback[-1]) | |
322 | elif rep.content.status == 'aborted': |
|
319 | elif rep.content.status == 'aborted': | |
323 | text = "ERROR: ABORTED\n" |
|
320 | text = "ERROR: ABORTED\n" | |
324 | ab = self.messages[rep.parent_header.msg_id].content |
|
321 | ab = self.messages[rep.parent_header.msg_id].content | |
325 | if 'code' in ab: |
|
322 | if 'code' in ab: | |
326 | text += ab.code |
|
323 | text += ab.code | |
327 | else: |
|
324 | else: | |
328 | text += ab |
|
325 | text += ab | |
329 | self.appendPlainText(text) |
|
326 | self.appendPlainText(text) | |
330 | self._show_prompt('>>> ') |
|
327 | self._show_prompt('>>> ') | |
331 | self.executed.emit(rep) |
|
328 | self.executed.emit(rep) | |
332 |
|
329 | |||
333 | #------ Communication methods ---------------------------------------------- |
|
330 | #------ Communication methods ---------------------------------------------- | |
334 |
|
331 | |||
335 | def _recv_output(self): |
|
332 | def _recv_output(self): | |
336 | omsgs = [] |
|
333 | omsgs = [] | |
337 | while True: |
|
334 | while True: | |
338 | omsg = self.session.recv(self.sub_socket) |
|
335 | omsg = self.session.recv(self.sub_socket) | |
339 | if omsg is None: |
|
336 | if omsg is None: | |
340 | break |
|
337 | break | |
341 | else: |
|
338 | else: | |
342 | omsgs.append(omsg) |
|
339 | omsgs.append(omsg) | |
343 | return omsgs |
|
340 | return omsgs | |
344 |
|
341 | |||
345 | def _recv_reply(self): |
|
342 | def _recv_reply(self): | |
346 | return self.session.recv(self.request_socket) |
|
343 | return self.session.recv(self.request_socket) | |
347 |
|
344 | |||
348 | def _recv_reply_now(self, msg_type): |
|
345 | def _recv_reply_now(self, msg_type): | |
349 | for i in xrange(5): |
|
346 | for i in xrange(5): | |
350 | rep = self._recv_reply() |
|
347 | rep = self._recv_reply() | |
351 | if rep is not None and rep.msg_type == msg_type: |
|
348 | if rep is not None and rep.msg_type == msg_type: | |
352 | return rep |
|
349 | return rep | |
353 | time.sleep(0.1) |
|
350 | time.sleep(0.1) | |
354 | return None |
|
351 | return None | |
355 |
|
352 | |||
356 |
|
353 | |||
357 | if __name__ == '__main__': |
|
354 | if __name__ == '__main__': | |
358 | import sys |
|
355 | import sys | |
359 |
|
356 | |||
360 | # Defaults |
|
357 | # Defaults | |
361 | ip = '127.0.0.1' |
|
358 | ip = '127.0.0.1' | |
362 | port_base = 5555 |
|
359 | port_base = 5555 | |
363 | connection = ('tcp://%s' % ip) + ':%i' |
|
360 | connection = ('tcp://%s' % ip) + ':%i' | |
364 | req_conn = connection % port_base |
|
361 | req_conn = connection % port_base | |
365 | sub_conn = connection % (port_base+1) |
|
362 | sub_conn = connection % (port_base+1) | |
366 |
|
363 | |||
367 | # Create initial sockets |
|
364 | # Create initial sockets | |
368 | c = zmq.Context() |
|
365 | c = zmq.Context() | |
369 | request_socket = c.socket(zmq.XREQ) |
|
366 | request_socket = c.socket(zmq.XREQ) | |
370 | request_socket.connect(req_conn) |
|
367 | request_socket.connect(req_conn) | |
371 | sub_socket = c.socket(zmq.SUB) |
|
368 | sub_socket = c.socket(zmq.SUB) | |
372 | sub_socket.connect(sub_conn) |
|
369 | sub_socket.connect(sub_conn) | |
373 | sub_socket.setsockopt(zmq.SUBSCRIBE, '') |
|
370 | sub_socket.setsockopt(zmq.SUBSCRIBE, '') | |
374 |
|
371 | |||
375 | # Launch application |
|
372 | # Launch application | |
376 | app = QtGui.QApplication(sys.argv) |
|
373 | app = QtGui.QApplication(sys.argv) | |
377 | widget = FrontendWidget(request_socket=request_socket, |
|
374 | widget = FrontendWidget(request_socket=request_socket, | |
378 | sub_socket=sub_socket) |
|
375 | sub_socket=sub_socket) | |
379 | widget.setWindowTitle('Python') |
|
376 | widget.setWindowTitle('Python') | |
380 | widget.resize(640, 480) |
|
377 | widget.resize(640, 480) | |
381 | widget.show() |
|
378 | widget.show() | |
382 | sys.exit(app.exec_()) |
|
379 | sys.exit(app.exec_()) | |
383 |
|
380 |
@@ -1,221 +1,183 b'' | |||||
1 | #------------------------------------------------------------------------------ |
|
1 | # System library imports. | |
2 | # Copyright (c) 2010, Enthought Inc |
|
|||
3 | # All rights reserved. |
|
|||
4 | # |
|
|||
5 | # This software is provided without warranty under the terms of the BSD license. |
|
|||
6 |
|
||||
7 | # |
|
|||
8 | # Author: Enthought Inc |
|
|||
9 | # Description: <Enthought pyface code editor> |
|
|||
10 | #------------------------------------------------------------------------------ |
|
|||
11 |
|
||||
12 | from PyQt4 import QtGui |
|
2 | from PyQt4 import QtGui | |
13 |
|
||||
14 | from pygments.lexer import RegexLexer, _TokenType, Text, Error |
|
3 | from pygments.lexer import RegexLexer, _TokenType, Text, Error | |
15 | from pygments.lexers import CLexer, CppLexer, PythonLexer |
|
4 | from pygments.lexers import CLexer, CppLexer, PythonLexer | |
16 | from pygments.styles.default import DefaultStyle |
|
5 | from pygments.styles.default import DefaultStyle | |
17 | from pygments.token import Comment |
|
6 | from pygments.token import Comment | |
18 |
|
7 | |||
19 |
|
8 | |||
20 | def get_tokens_unprocessed(self, text, stack=('root',)): |
|
9 | def get_tokens_unprocessed(self, text, stack=('root',)): | |
21 | """ Split ``text`` into (tokentype, text) pairs. |
|
10 | """ Split ``text`` into (tokentype, text) pairs. | |
22 |
|
11 | |||
23 | Monkeypatched to store the final stack on the object itself. |
|
12 | Monkeypatched to store the final stack on the object itself. | |
24 | """ |
|
13 | """ | |
25 | pos = 0 |
|
14 | pos = 0 | |
26 | tokendefs = self._tokens |
|
15 | tokendefs = self._tokens | |
27 |
if hasattr(self, '_e |
|
16 | if hasattr(self, '_saved_state_stack'): | |
28 |
statestack = list(self._e |
|
17 | statestack = list(self._saved_state_stack) | |
29 | else: |
|
18 | else: | |
30 | statestack = list(stack) |
|
19 | statestack = list(stack) | |
31 | statetokens = tokendefs[statestack[-1]] |
|
20 | statetokens = tokendefs[statestack[-1]] | |
32 | while 1: |
|
21 | while 1: | |
33 | for rexmatch, action, new_state in statetokens: |
|
22 | for rexmatch, action, new_state in statetokens: | |
34 | m = rexmatch(text, pos) |
|
23 | m = rexmatch(text, pos) | |
35 | if m: |
|
24 | if m: | |
36 | if type(action) is _TokenType: |
|
25 | if type(action) is _TokenType: | |
37 | yield pos, action, m.group() |
|
26 | yield pos, action, m.group() | |
38 | else: |
|
27 | else: | |
39 | for item in action(self, m): |
|
28 | for item in action(self, m): | |
40 | yield item |
|
29 | yield item | |
41 | pos = m.end() |
|
30 | pos = m.end() | |
42 | if new_state is not None: |
|
31 | if new_state is not None: | |
43 | # state transition |
|
32 | # state transition | |
44 | if isinstance(new_state, tuple): |
|
33 | if isinstance(new_state, tuple): | |
45 | for state in new_state: |
|
34 | for state in new_state: | |
46 | if state == '#pop': |
|
35 | if state == '#pop': | |
47 | statestack.pop() |
|
36 | statestack.pop() | |
48 | elif state == '#push': |
|
37 | elif state == '#push': | |
49 | statestack.append(statestack[-1]) |
|
38 | statestack.append(statestack[-1]) | |
50 | else: |
|
39 | else: | |
51 | statestack.append(state) |
|
40 | statestack.append(state) | |
52 | elif isinstance(new_state, int): |
|
41 | elif isinstance(new_state, int): | |
53 | # pop |
|
42 | # pop | |
54 | del statestack[new_state:] |
|
43 | del statestack[new_state:] | |
55 | elif new_state == '#push': |
|
44 | elif new_state == '#push': | |
56 | statestack.append(statestack[-1]) |
|
45 | statestack.append(statestack[-1]) | |
57 | else: |
|
46 | else: | |
58 | assert False, "wrong state def: %r" % new_state |
|
47 | assert False, "wrong state def: %r" % new_state | |
59 | statetokens = tokendefs[statestack[-1]] |
|
48 | statetokens = tokendefs[statestack[-1]] | |
60 | break |
|
49 | break | |
61 | else: |
|
50 | else: | |
62 | try: |
|
51 | try: | |
63 | if text[pos] == '\n': |
|
52 | if text[pos] == '\n': | |
64 | # at EOL, reset state to "root" |
|
53 | # at EOL, reset state to "root" | |
65 | pos += 1 |
|
54 | pos += 1 | |
66 | statestack = ['root'] |
|
55 | statestack = ['root'] | |
67 | statetokens = tokendefs['root'] |
|
56 | statetokens = tokendefs['root'] | |
68 | yield pos, Text, u'\n' |
|
57 | yield pos, Text, u'\n' | |
69 | continue |
|
58 | continue | |
70 | yield pos, Error, text[pos] |
|
59 | yield pos, Error, text[pos] | |
71 | pos += 1 |
|
60 | pos += 1 | |
72 | except IndexError: |
|
61 | except IndexError: | |
73 | break |
|
62 | break | |
74 |
self._e |
|
63 | self._saved_state_stack = list(statestack) | |
75 |
|
64 | |||
76 | # Monkeypatch! |
|
65 | # Monkeypatch! | |
77 | RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed |
|
66 | RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed | |
78 |
|
67 | |||
79 |
|
68 | |||
80 | # Even with the above monkey patch to store state, multiline comments do not |
|
|||
81 | # work since they are stateless (Pygments uses a single multiline regex for |
|
|||
82 | # these comments, but Qt lexes by line). So we need to add a state for comments |
|
|||
83 | # to the C and C++ lexers. This means that nested multiline comments will appear |
|
|||
84 | # to be valid C/C++, but this is better than the alternative for now. |
|
|||
85 |
|
||||
86 | def replace_pattern(tokens, new_pattern): |
|
|||
87 | """ Given a RegexLexer token dictionary 'tokens', replace all patterns that |
|
|||
88 | match the token specified in 'new_pattern' with 'new_pattern'. |
|
|||
89 | """ |
|
|||
90 | for state in tokens.values(): |
|
|||
91 | for index, pattern in enumerate(state): |
|
|||
92 | if isinstance(pattern, tuple) and pattern[1] == new_pattern[1]: |
|
|||
93 | state[index] = new_pattern |
|
|||
94 |
|
||||
95 | # More monkeypatching! |
|
|||
96 | comment_start = (r'/\*', Comment.Multiline, 'comment') |
|
|||
97 | comment_state = [ (r'[^*/]', Comment.Multiline), |
|
|||
98 | (r'/\*', Comment.Multiline, '#push'), |
|
|||
99 | (r'\*/', Comment.Multiline, '#pop'), |
|
|||
100 | (r'[*/]', Comment.Multiline) ] |
|
|||
101 | replace_pattern(CLexer.tokens, comment_start) |
|
|||
102 | replace_pattern(CppLexer.tokens, comment_start) |
|
|||
103 | CLexer.tokens['comment'] = comment_state |
|
|||
104 | CppLexer.tokens['comment'] = comment_state |
|
|||
105 |
|
||||
106 |
|
||||
107 | class BlockUserData(QtGui.QTextBlockUserData): |
|
69 | class BlockUserData(QtGui.QTextBlockUserData): | |
108 | """ Storage for the user data associated with each line. |
|
70 | """ Storage for the user data associated with each line. | |
109 | """ |
|
71 | """ | |
110 |
|
72 | |||
111 | syntax_stack = ('root',) |
|
73 | syntax_stack = ('root',) | |
112 |
|
74 | |||
113 | def __init__(self, **kwds): |
|
75 | def __init__(self, **kwds): | |
114 | for key, value in kwds.iteritems(): |
|
76 | for key, value in kwds.iteritems(): | |
115 | setattr(self, key, value) |
|
77 | setattr(self, key, value) | |
116 | QtGui.QTextBlockUserData.__init__(self) |
|
78 | QtGui.QTextBlockUserData.__init__(self) | |
117 |
|
79 | |||
118 | def __repr__(self): |
|
80 | def __repr__(self): | |
119 | attrs = ['syntax_stack'] |
|
81 | attrs = ['syntax_stack'] | |
120 | kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr)) |
|
82 | kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr)) | |
121 | for attr in attrs ]) |
|
83 | for attr in attrs ]) | |
122 | return 'BlockUserData(%s)' % kwds |
|
84 | return 'BlockUserData(%s)' % kwds | |
123 |
|
85 | |||
124 |
|
86 | |||
125 | class PygmentsHighlighter(QtGui.QSyntaxHighlighter): |
|
87 | class PygmentsHighlighter(QtGui.QSyntaxHighlighter): | |
126 | """ Syntax highlighter that uses Pygments for parsing. """ |
|
88 | """ Syntax highlighter that uses Pygments for parsing. """ | |
127 |
|
89 | |||
128 | def __init__(self, parent, lexer=None): |
|
90 | def __init__(self, parent, lexer=None): | |
129 | super(PygmentsHighlighter, self).__init__(parent) |
|
91 | super(PygmentsHighlighter, self).__init__(parent) | |
130 |
|
92 | |||
131 | self._lexer = lexer if lexer else PythonLexer() |
|
93 | self._lexer = lexer if lexer else PythonLexer() | |
132 | self._style = DefaultStyle |
|
94 | self._style = DefaultStyle | |
133 | # Caches for formats and brushes. |
|
95 | # Caches for formats and brushes. | |
134 | self._brushes = {} |
|
96 | self._brushes = {} | |
135 | self._formats = {} |
|
97 | self._formats = {} | |
136 |
|
98 | |||
137 | def highlightBlock(self, qstring): |
|
99 | def highlightBlock(self, qstring): | |
138 | """ Highlight a block of text. |
|
100 | """ Highlight a block of text. | |
139 | """ |
|
101 | """ | |
140 | qstring = unicode(qstring) |
|
102 | qstring = unicode(qstring) | |
141 | prev_data = self.previous_block_data() |
|
103 | prev_data = self.previous_block_data() | |
142 |
|
104 | |||
143 | if prev_data is not None: |
|
105 | if prev_data is not None: | |
144 |
self._lexer._e |
|
106 | self._lexer._saved_state_stack = prev_data.syntax_stack | |
145 |
elif hasattr(self._lexer, '_e |
|
107 | elif hasattr(self._lexer, '_saved_state_stack'): | |
146 |
del self._lexer._e |
|
108 | del self._lexer._saved_state_stack | |
147 |
|
109 | |||
148 | index = 0 |
|
110 | index = 0 | |
149 | # Lex the text using Pygments |
|
111 | # Lex the text using Pygments | |
150 | for token, text in self._lexer.get_tokens(qstring): |
|
112 | for token, text in self._lexer.get_tokens(qstring): | |
151 | l = len(text) |
|
113 | l = len(text) | |
152 | format = self._get_format(token) |
|
114 | format = self._get_format(token) | |
153 | if format is not None: |
|
115 | if format is not None: | |
154 | self.setFormat(index, l, format) |
|
116 | self.setFormat(index, l, format) | |
155 | index += l |
|
117 | index += l | |
156 |
|
118 | |||
157 |
if hasattr(self._lexer, '_e |
|
119 | if hasattr(self._lexer, '_saved_state_stack'): | |
158 |
data = BlockUserData(syntax_stack=self._lexer._e |
|
120 | data = BlockUserData(syntax_stack=self._lexer._saved_state_stack) | |
159 | self.currentBlock().setUserData(data) |
|
121 | self.currentBlock().setUserData(data) | |
160 | # Clean up for the next go-round. |
|
122 | # Clean up for the next go-round. | |
161 |
del self._lexer._e |
|
123 | del self._lexer._saved_state_stack | |
162 |
|
124 | |||
163 | def previous_block_data(self): |
|
125 | def previous_block_data(self): | |
164 | """ Convenience method for returning the previous block's user data. |
|
126 | """ Convenience method for returning the previous block's user data. | |
165 | """ |
|
127 | """ | |
166 | return self.currentBlock().previous().userData() |
|
128 | return self.currentBlock().previous().userData() | |
167 |
|
129 | |||
168 | def _get_format(self, token): |
|
130 | def _get_format(self, token): | |
169 | """ Returns a QTextCharFormat for token or None. |
|
131 | """ Returns a QTextCharFormat for token or None. | |
170 | """ |
|
132 | """ | |
171 | if token in self._formats: |
|
133 | if token in self._formats: | |
172 | return self._formats[token] |
|
134 | return self._formats[token] | |
173 | result = None |
|
135 | result = None | |
174 | for key, value in self._style.style_for_token(token) .items(): |
|
136 | for key, value in self._style.style_for_token(token) .items(): | |
175 | if value: |
|
137 | if value: | |
176 | if result is None: |
|
138 | if result is None: | |
177 | result = QtGui.QTextCharFormat() |
|
139 | result = QtGui.QTextCharFormat() | |
178 | if key == 'color': |
|
140 | if key == 'color': | |
179 | result.setForeground(self._get_brush(value)) |
|
141 | result.setForeground(self._get_brush(value)) | |
180 | elif key == 'bgcolor': |
|
142 | elif key == 'bgcolor': | |
181 | result.setBackground(self._get_brush(value)) |
|
143 | result.setBackground(self._get_brush(value)) | |
182 | elif key == 'bold': |
|
144 | elif key == 'bold': | |
183 | result.setFontWeight(QtGui.QFont.Bold) |
|
145 | result.setFontWeight(QtGui.QFont.Bold) | |
184 | elif key == 'italic': |
|
146 | elif key == 'italic': | |
185 | result.setFontItalic(True) |
|
147 | result.setFontItalic(True) | |
186 | elif key == 'underline': |
|
148 | elif key == 'underline': | |
187 | result.setUnderlineStyle( |
|
149 | result.setUnderlineStyle( | |
188 | QtGui.QTextCharFormat.SingleUnderline) |
|
150 | QtGui.QTextCharFormat.SingleUnderline) | |
189 | elif key == 'sans': |
|
151 | elif key == 'sans': | |
190 | result.setFontStyleHint(QtGui.QFont.SansSerif) |
|
152 | result.setFontStyleHint(QtGui.QFont.SansSerif) | |
191 | elif key == 'roman': |
|
153 | elif key == 'roman': | |
192 | result.setFontStyleHint(QtGui.QFont.Times) |
|
154 | result.setFontStyleHint(QtGui.QFont.Times) | |
193 | elif key == 'mono': |
|
155 | elif key == 'mono': | |
194 | result.setFontStyleHint(QtGui.QFont.TypeWriter) |
|
156 | result.setFontStyleHint(QtGui.QFont.TypeWriter) | |
195 | elif key == 'border': |
|
157 | elif key == 'border': | |
196 | # Borders are normally used for errors. We can't do a border |
|
158 | # Borders are normally used for errors. We can't do a border | |
197 | # so instead we do a wavy underline |
|
159 | # so instead we do a wavy underline | |
198 | result.setUnderlineStyle( |
|
160 | result.setUnderlineStyle( | |
199 | QtGui.QTextCharFormat.WaveUnderline) |
|
161 | QtGui.QTextCharFormat.WaveUnderline) | |
200 | result.setUnderlineColor(self._get_color(value)) |
|
162 | result.setUnderlineColor(self._get_color(value)) | |
201 | self._formats[token] = result |
|
163 | self._formats[token] = result | |
202 | return result |
|
164 | return result | |
203 |
|
165 | |||
204 | def _get_brush(self, color): |
|
166 | def _get_brush(self, color): | |
205 | """ Returns a brush for the color. |
|
167 | """ Returns a brush for the color. | |
206 | """ |
|
168 | """ | |
207 | result = self._brushes.get(color) |
|
169 | result = self._brushes.get(color) | |
208 | if result is None: |
|
170 | if result is None: | |
209 | qcolor = self._get_color(color) |
|
171 | qcolor = self._get_color(color) | |
210 | result = QtGui.QBrush(qcolor) |
|
172 | result = QtGui.QBrush(qcolor) | |
211 | self._brushes[color] = result |
|
173 | self._brushes[color] = result | |
212 |
|
174 | |||
213 | return result |
|
175 | return result | |
214 |
|
176 | |||
215 | def _get_color(self, color): |
|
177 | def _get_color(self, color): | |
216 | qcolor = QtGui.QColor() |
|
178 | qcolor = QtGui.QColor() | |
217 | qcolor.setRgb(int(color[:2],base=16), |
|
179 | qcolor.setRgb(int(color[:2],base=16), | |
218 | int(color[2:4], base=16), |
|
180 | int(color[2:4], base=16), | |
219 | int(color[4:6], base=16)) |
|
181 | int(color[4:6], base=16)) | |
220 | return qcolor |
|
182 | return qcolor | |
221 |
|
183 |
General Comments 0
You need to be logged in to leave comments.
Login now