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