Show More
@@ -0,0 +1,156 b'' | |||
|
1 | # System library imports | |
|
2 | from PyQt4 import QtCore, QtGui | |
|
3 | ||
|
4 | ||
|
5 | class CallTipWidget(QtGui.QLabel): | |
|
6 | """ Shows call tips by parsing the current text of Q[Plain]TextEdit. | |
|
7 | """ | |
|
8 | ||
|
9 | #-------------------------------------------------------------------------- | |
|
10 | # 'QWidget' interface | |
|
11 | #-------------------------------------------------------------------------- | |
|
12 | ||
|
13 | def __init__(self, parent): | |
|
14 | """ Create a call tip manager that is attached to the specified Qt | |
|
15 | text edit widget. | |
|
16 | """ | |
|
17 | assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) | |
|
18 | QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip) | |
|
19 | ||
|
20 | self.setFont(parent.document().defaultFont()) | |
|
21 | self.setForegroundRole(QtGui.QPalette.ToolTipText) | |
|
22 | self.setBackgroundRole(QtGui.QPalette.ToolTipBase) | |
|
23 | self.setPalette(QtGui.QToolTip.palette()) | |
|
24 | ||
|
25 | self.setAlignment(QtCore.Qt.AlignLeft) | |
|
26 | self.setIndent(1) | |
|
27 | self.setFrameStyle(QtGui.QFrame.NoFrame) | |
|
28 | self.setMargin(1 + self.style().pixelMetric( | |
|
29 | QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self)) | |
|
30 | self.setWindowOpacity(self.style().styleHint( | |
|
31 | QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0) | |
|
32 | ||
|
33 | def hideEvent(self, event): | |
|
34 | """ Reimplemented to disconnect the cursor movement handler. | |
|
35 | """ | |
|
36 | QtGui.QListWidget.hideEvent(self, event) | |
|
37 | self.parent().cursorPositionChanged.disconnect(self._update_tip) | |
|
38 | ||
|
39 | def paintEvent(self, event): | |
|
40 | """ Reimplemented to paint the background panel. | |
|
41 | """ | |
|
42 | painter = QtGui.QStylePainter(self) | |
|
43 | option = QtGui.QStyleOptionFrame() | |
|
44 | option.init(self) | |
|
45 | painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option) | |
|
46 | painter.end() | |
|
47 | ||
|
48 | QtGui.QLabel.paintEvent(self, event) | |
|
49 | ||
|
50 | def showEvent(self, event): | |
|
51 | """ Reimplemented to connect the cursor movement handler. | |
|
52 | """ | |
|
53 | QtGui.QListWidget.showEvent(self, event) | |
|
54 | self.parent().cursorPositionChanged.connect(self._update_tip) | |
|
55 | ||
|
56 | #-------------------------------------------------------------------------- | |
|
57 | # 'CallTipWidget' interface | |
|
58 | #-------------------------------------------------------------------------- | |
|
59 | ||
|
60 | def show_tip(self, tip): | |
|
61 | """ Attempts to show the specified tip at the current cursor location. | |
|
62 | """ | |
|
63 | text_edit = self.parent() | |
|
64 | document = text_edit.document() | |
|
65 | cursor = text_edit.textCursor() | |
|
66 | search_pos = cursor.position() - 1 | |
|
67 | self._start_position, _ = self._find_parenthesis(search_pos, | |
|
68 | forward=False) | |
|
69 | if self._start_position == -1: | |
|
70 | return False | |
|
71 | ||
|
72 | point = text_edit.cursorRect(cursor).bottomRight() | |
|
73 | point = text_edit.mapToGlobal(point) | |
|
74 | self.move(point) | |
|
75 | self.setText(tip) | |
|
76 | if self.isVisible(): | |
|
77 | self.resize(self.sizeHint()) | |
|
78 | else: | |
|
79 | self.show() | |
|
80 | return True | |
|
81 | ||
|
82 | #-------------------------------------------------------------------------- | |
|
83 | # Protected interface | |
|
84 | #-------------------------------------------------------------------------- | |
|
85 | ||
|
86 | def _find_parenthesis(self, position, forward=True): | |
|
87 | """ If 'forward' is True (resp. False), proceed forwards | |
|
88 | (resp. backwards) through the line that contains 'position' until an | |
|
89 | unmatched closing (resp. opening) parenthesis is found. Returns a | |
|
90 | tuple containing the position of this parenthesis (or -1 if it is | |
|
91 | not found) and the number commas (at depth 0) found along the way. | |
|
92 | """ | |
|
93 | commas = depth = 0 | |
|
94 | document = self.parent().document() | |
|
95 | qchar = document.characterAt(position) | |
|
96 | while (position > 0 and qchar.isPrint() and | |
|
97 | # Need to check explicitly for line/paragraph separators: | |
|
98 | qchar.unicode() not in (0x2028, 0x2029)): | |
|
99 | char = qchar.toAscii() | |
|
100 | if char == ',' and depth == 0: | |
|
101 | commas += 1 | |
|
102 | elif char == ')': | |
|
103 | if forward and depth == 0: | |
|
104 | break | |
|
105 | depth += 1 | |
|
106 | elif char == '(': | |
|
107 | if not forward and depth == 0: | |
|
108 | break | |
|
109 | depth -= 1 | |
|
110 | position += 1 if forward else -1 | |
|
111 | qchar = document.characterAt(position) | |
|
112 | else: | |
|
113 | position = -1 | |
|
114 | return position, commas | |
|
115 | ||
|
116 | def _highlight_tip(self, tip, current_argument): | |
|
117 | """ Highlight the current argument (arguments start at 0), ending at the | |
|
118 | next comma or unmatched closing parenthesis. | |
|
119 | ||
|
120 | FIXME: This is an unreliable way to do things and it isn't being | |
|
121 | used right now. Instead, we should use inspect.getargspec | |
|
122 | metadata for this purpose. | |
|
123 | """ | |
|
124 | start = tip.find('(') | |
|
125 | if start != -1: | |
|
126 | for i in xrange(current_argument): | |
|
127 | start = tip.find(',', start) | |
|
128 | if start != -1: | |
|
129 | end = start + 1 | |
|
130 | while end < len(tip): | |
|
131 | char = tip[end] | |
|
132 | depth = 0 | |
|
133 | if (char == ',' and depth == 0): | |
|
134 | break | |
|
135 | elif char == '(': | |
|
136 | depth += 1 | |
|
137 | elif char == ')': | |
|
138 | if depth == 0: | |
|
139 | break | |
|
140 | depth -= 1 | |
|
141 | end += 1 | |
|
142 | tip = tip[:start+1] + '<font color="blue">' + \ | |
|
143 | tip[start+1:end] + '</font>' + tip[end:] | |
|
144 | tip = tip.replace('\n', '<br/>') | |
|
145 | return tip | |
|
146 | ||
|
147 | def _update_tip(self): | |
|
148 | """ Updates the tip based on user cursor movement. | |
|
149 | """ | |
|
150 | cursor = self.parent().textCursor() | |
|
151 | if cursor.position() <= self._start_position: | |
|
152 | self.hide() | |
|
153 | else: | |
|
154 | position, commas = self._find_parenthesis(self._start_position + 1) | |
|
155 | if position != -1: | |
|
156 | self.hide() |
@@ -0,0 +1,57 b'' | |||
|
1 | # System library imports | |
|
2 | from pygments.token import Token, is_token_subtype | |
|
3 | ||
|
4 | ||
|
5 | class CompletionLexer(object): | |
|
6 | """ Uses Pygments and some auxillary information to lex code snippets for | |
|
7 | symbol contexts. | |
|
8 | """ | |
|
9 | ||
|
10 | # Maps Lexer names to a list of possible name separators | |
|
11 | separator_map = { 'C' : [ '.', '->' ], | |
|
12 | 'C++' : [ '.', '->', '::' ], | |
|
13 | 'Python' : [ '.' ] } | |
|
14 | ||
|
15 | def __init__(self, lexer): | |
|
16 | self.lexer = lexer | |
|
17 | ||
|
18 | def get_context(self, string): | |
|
19 | """ Assuming the cursor is at the end of the specified string, get the | |
|
20 | context (a list of names) for the symbol at cursor position. | |
|
21 | """ | |
|
22 | context = [] | |
|
23 | reversed_tokens = list(self._lexer.get_tokens(string)) | |
|
24 | reversed_tokens.reverse() | |
|
25 | ||
|
26 | # Pygments often tacks on a newline when none is specified in the input | |
|
27 | if reversed_tokens and reversed_tokens[0][1].endswith('\n') and \ | |
|
28 | not string.endswith('\n'): | |
|
29 | reversed_tokens.pop(0) | |
|
30 | ||
|
31 | current_op = unicode() | |
|
32 | for token, text in reversed_tokens: | |
|
33 | if is_token_subtype(token, Token.Name) and \ | |
|
34 | (not context or current_op in self._name_separators): | |
|
35 | if not context and current_op in self._name_separators: | |
|
36 | context.insert(0, unicode()) | |
|
37 | context.insert(0, text) | |
|
38 | current_op = unicode() | |
|
39 | elif token is Token.Operator or token is Token.Punctuation: | |
|
40 | current_op = text + current_op | |
|
41 | else: | |
|
42 | break | |
|
43 | ||
|
44 | return context | |
|
45 | ||
|
46 | def get_lexer(self, lexer): | |
|
47 | return self._lexer | |
|
48 | ||
|
49 | def set_lexer(self, lexer, name_separators=None): | |
|
50 | self._lexer = lexer | |
|
51 | if name_separators is None: | |
|
52 | self._name_separators = self.separator_map.get(lexer.name, ['.']) | |
|
53 | else: | |
|
54 | self._name_separators = list(name_separators) | |
|
55 | ||
|
56 | lexer = property(get_lexer, set_lexer) | |
|
57 |
@@ -0,0 +1,121 b'' | |||
|
1 | # System library imports | |
|
2 | from PyQt4 import QtCore, QtGui | |
|
3 | ||
|
4 | ||
|
5 | class CompletionWidget(QtGui.QListWidget): | |
|
6 | """ A widget for GUI tab completion. | |
|
7 | """ | |
|
8 | ||
|
9 | #-------------------------------------------------------------------------- | |
|
10 | # 'QWidget' interface | |
|
11 | #-------------------------------------------------------------------------- | |
|
12 | ||
|
13 | def __init__(self, parent): | |
|
14 | """ Create a completion widget that is attached to the specified Qt | |
|
15 | text edit widget. | |
|
16 | """ | |
|
17 | assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) | |
|
18 | QtGui.QListWidget.__init__(self, parent) | |
|
19 | ||
|
20 | self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint) | |
|
21 | self.setAttribute(QtCore.Qt.WA_StaticContents) | |
|
22 | ||
|
23 | # Ensure that parent keeps focus when widget is displayed. | |
|
24 | self.setFocusProxy(parent) | |
|
25 | ||
|
26 | self.setFrameShadow(QtGui.QFrame.Plain) | |
|
27 | self.setFrameShape(QtGui.QFrame.StyledPanel) | |
|
28 | ||
|
29 | self.itemActivated.connect(self._complete_current) | |
|
30 | ||
|
31 | def hideEvent(self, event): | |
|
32 | """ Reimplemented to disconnect the cursor movement handler. | |
|
33 | """ | |
|
34 | QtGui.QListWidget.hideEvent(self, event) | |
|
35 | self.parent().cursorPositionChanged.disconnect(self._update_current) | |
|
36 | ||
|
37 | def keyPressEvent(self, event): | |
|
38 | """ Reimplemented to update the list. | |
|
39 | """ | |
|
40 | key, text = event.key(), event.text() | |
|
41 | ||
|
42 | if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter, | |
|
43 | QtCore.Qt.Key_Tab): | |
|
44 | self._complete_current() | |
|
45 | event.accept() | |
|
46 | ||
|
47 | elif key == QtCore.Qt.Key_Escape: | |
|
48 | self.hide() | |
|
49 | event.accept() | |
|
50 | ||
|
51 | elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, | |
|
52 | QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown, | |
|
53 | QtCore.Qt.Key_Home, QtCore.Qt.Key_End): | |
|
54 | QtGui.QListWidget.keyPressEvent(self, event) | |
|
55 | event.accept() | |
|
56 | ||
|
57 | else: | |
|
58 | event.ignore() | |
|
59 | ||
|
60 | def showEvent(self, event): | |
|
61 | """ Reimplemented to connect the cursor movement handler. | |
|
62 | """ | |
|
63 | QtGui.QListWidget.showEvent(self, event) | |
|
64 | self.parent().cursorPositionChanged.connect(self._update_current) | |
|
65 | ||
|
66 | #-------------------------------------------------------------------------- | |
|
67 | # 'CompletionWidget' interface | |
|
68 | #-------------------------------------------------------------------------- | |
|
69 | ||
|
70 | def show_items(self, cursor, items): | |
|
71 | """ Shows the completion widget with 'items' at the position specified | |
|
72 | by 'cursor'. | |
|
73 | """ | |
|
74 | text_edit = self.parent() | |
|
75 | point = text_edit.cursorRect(cursor).bottomRight() | |
|
76 | point = text_edit.mapToGlobal(point) | |
|
77 | screen_rect = QtGui.QApplication.desktop().availableGeometry(self) | |
|
78 | if screen_rect.size().height() - point.y() - self.height() < 0: | |
|
79 | point = text_edit.mapToGlobal(text_edit.cursorRect().topRight()) | |
|
80 | point.setY(point.y() - self.height()) | |
|
81 | self.move(point) | |
|
82 | ||
|
83 | self._start_position = cursor.position() | |
|
84 | self.clear() | |
|
85 | self.addItems(items) | |
|
86 | self.setCurrentRow(0) | |
|
87 | self.show() | |
|
88 | ||
|
89 | #-------------------------------------------------------------------------- | |
|
90 | # Protected interface | |
|
91 | #-------------------------------------------------------------------------- | |
|
92 | ||
|
93 | def _complete_current(self): | |
|
94 | """ Perform the completion with the currently selected item. | |
|
95 | """ | |
|
96 | self._current_text_cursor().insertText(self.currentItem().text()) | |
|
97 | self.hide() | |
|
98 | ||
|
99 | def _current_text_cursor(self): | |
|
100 | """ Returns a cursor with text between the start position and the | |
|
101 | current position selected. | |
|
102 | """ | |
|
103 | cursor = self.parent().textCursor() | |
|
104 | if cursor.position() >= self._start_position: | |
|
105 | cursor.setPosition(self._start_position, | |
|
106 | QtGui.QTextCursor.KeepAnchor) | |
|
107 | return cursor | |
|
108 | ||
|
109 | def _update_current(self): | |
|
110 | """ Updates the current item based on the current text. | |
|
111 | """ | |
|
112 | prefix = self._current_text_cursor().selectedText() | |
|
113 | if prefix: | |
|
114 | items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith | | |
|
115 | QtCore.Qt.MatchCaseSensitive)) | |
|
116 | if items: | |
|
117 | self.setCurrentItem(items[0]) | |
|
118 | else: | |
|
119 | self.hide() | |
|
120 | else: | |
|
121 | self.hide() |
This diff has been collapsed as it changes many lines, (650 lines changed) Show them Hide them | |||
@@ -0,0 +1,650 b'' | |||
|
1 | # Standard library imports | |
|
2 | import re | |
|
3 | ||
|
4 | # System library imports | |
|
5 | from PyQt4 import QtCore, QtGui | |
|
6 | ||
|
7 | # Local imports | |
|
8 | from completion_widget import CompletionWidget | |
|
9 | ||
|
10 | ||
|
11 | class AnsiCodeProcessor(object): | |
|
12 | """ Translates ANSI escape codes into readable attributes. | |
|
13 | """ | |
|
14 | ||
|
15 | def __init__(self): | |
|
16 | self.ansi_colors = ( # Normal, Bright/Light | |
|
17 | ('#000000', '#7f7f7f'), # 0: black | |
|
18 | ('#cd0000', '#ff0000'), # 1: red | |
|
19 | ('#00cd00', '#00ff00'), # 2: green | |
|
20 | ('#cdcd00', '#ffff00'), # 3: yellow | |
|
21 | ('#0000ee', '#0000ff'), # 4: blue | |
|
22 | ('#cd00cd', '#ff00ff'), # 5: magenta | |
|
23 | ('#00cdcd', '#00ffff'), # 6: cyan | |
|
24 | ('#e5e5e5', '#ffffff')) # 7: white | |
|
25 | self.reset() | |
|
26 | ||
|
27 | def set_code(self, code): | |
|
28 | """ Set attributes based on code. | |
|
29 | """ | |
|
30 | if code == 0: | |
|
31 | self.reset() | |
|
32 | elif code == 1: | |
|
33 | self.intensity = 1 | |
|
34 | self.bold = True | |
|
35 | elif code == 3: | |
|
36 | self.italic = True | |
|
37 | elif code == 4: | |
|
38 | self.underline = True | |
|
39 | elif code == 22: | |
|
40 | self.intensity = 0 | |
|
41 | self.bold = False | |
|
42 | elif code == 23: | |
|
43 | self.italic = False | |
|
44 | elif code == 24: | |
|
45 | self.underline = False | |
|
46 | elif code >= 30 and code <= 37: | |
|
47 | self.foreground_color = code - 30 | |
|
48 | elif code == 39: | |
|
49 | self.foreground_color = None | |
|
50 | elif code >= 40 and code <= 47: | |
|
51 | self.background_color = code - 40 | |
|
52 | elif code == 49: | |
|
53 | self.background_color = None | |
|
54 | ||
|
55 | def reset(self): | |
|
56 | """ Reset attributs to their default values. | |
|
57 | """ | |
|
58 | self.intensity = 0 | |
|
59 | self.italic = False | |
|
60 | self.bold = False | |
|
61 | self.underline = False | |
|
62 | self.foreground_color = None | |
|
63 | self.background_color = None | |
|
64 | ||
|
65 | ||
|
66 | class QtAnsiCodeProcessor(AnsiCodeProcessor): | |
|
67 | """ Translates ANSI escape codes into QTextCharFormats. | |
|
68 | """ | |
|
69 | ||
|
70 | def get_format(self): | |
|
71 | """ Returns a QTextCharFormat that encodes the current style attributes. | |
|
72 | """ | |
|
73 | format = QtGui.QTextCharFormat() | |
|
74 | ||
|
75 | # Set foreground color | |
|
76 | if self.foreground_color is not None: | |
|
77 | color = self.ansi_colors[self.foreground_color][self.intensity] | |
|
78 | format.setForeground(QtGui.QColor(color)) | |
|
79 | ||
|
80 | # Set background color | |
|
81 | if self.background_color is not None: | |
|
82 | color = self.ansi_colors[self.background_color][self.intensity] | |
|
83 | format.setBackground(QtGui.QColor(color)) | |
|
84 | ||
|
85 | # Set font weight/style options | |
|
86 | if self.bold: | |
|
87 | format.setFontWeight(QtGui.QFont.Bold) | |
|
88 | else: | |
|
89 | format.setFontWeight(QtGui.QFont.Normal) | |
|
90 | format.setFontItalic(self.italic) | |
|
91 | format.setFontUnderline(self.underline) | |
|
92 | ||
|
93 | return format | |
|
94 | ||
|
95 | ||
|
96 | class ConsoleWidget(QtGui.QPlainTextEdit): | |
|
97 | """ Base class for console-type widgets. This class is mainly concerned with | |
|
98 | dealing with the prompt, keeping the cursor inside the editing line, and | |
|
99 | handling ANSI escape sequences. | |
|
100 | """ | |
|
101 | ||
|
102 | # Regex to match ANSI escape sequences | |
|
103 | _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?') | |
|
104 | ||
|
105 | # When ctrl is pressed, map certain keys to other keys (without the ctrl): | |
|
106 | _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left, | |
|
107 | QtCore.Qt.Key_F : QtCore.Qt.Key_Right, | |
|
108 | QtCore.Qt.Key_A : QtCore.Qt.Key_Home, | |
|
109 | QtCore.Qt.Key_E : QtCore.Qt.Key_End, | |
|
110 | QtCore.Qt.Key_P : QtCore.Qt.Key_Up, | |
|
111 | QtCore.Qt.Key_N : QtCore.Qt.Key_Down, | |
|
112 | QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, } | |
|
113 | ||
|
114 | #--------------------------------------------------------------------------- | |
|
115 | # 'QWidget' interface | |
|
116 | #--------------------------------------------------------------------------- | |
|
117 | ||
|
118 | def __init__(self, parent=None): | |
|
119 | QtGui.QPlainTextEdit.__init__(self, parent) | |
|
120 | ||
|
121 | # Initialize public and protected variables | |
|
122 | self.ansi_codes = True | |
|
123 | self.continuation_prompt = '> ' | |
|
124 | self.gui_completion = True | |
|
125 | self._ansi_processor = QtAnsiCodeProcessor() | |
|
126 | self._completion_widget = CompletionWidget(self) | |
|
127 | self._executing = False | |
|
128 | self._prompt = '' | |
|
129 | self._prompt_pos = 0 | |
|
130 | self._reading = False | |
|
131 | ||
|
132 | # Configure some basic QPlainTextEdit settings | |
|
133 | self.setLineWrapMode(QtGui.QPlainTextEdit.WidgetWidth) | |
|
134 | self.setMaximumBlockCount(500) # Limit text buffer size | |
|
135 | self.setUndoRedoEnabled(False) | |
|
136 | ||
|
137 | # Set a monospaced font | |
|
138 | point_size = QtGui.QApplication.font().pointSize() | |
|
139 | font = QtGui.QFont('Monospace', point_size) | |
|
140 | font.setStyleHint(QtGui.QFont.TypeWriter) | |
|
141 | self._completion_widget.setFont(font) | |
|
142 | self.document().setDefaultFont(font) | |
|
143 | ||
|
144 | # Define a custom context menu | |
|
145 | self._context_menu = QtGui.QMenu(self) | |
|
146 | ||
|
147 | copy_action = QtGui.QAction('Copy', self) | |
|
148 | copy_action.triggered.connect(self.copy) | |
|
149 | self.copyAvailable.connect(copy_action.setEnabled) | |
|
150 | self._context_menu.addAction(copy_action) | |
|
151 | ||
|
152 | self._paste_action = QtGui.QAction('Paste', self) | |
|
153 | self._paste_action.triggered.connect(self.paste) | |
|
154 | self._context_menu.addAction(self._paste_action) | |
|
155 | self._context_menu.addSeparator() | |
|
156 | ||
|
157 | select_all_action = QtGui.QAction('Select All', self) | |
|
158 | select_all_action.triggered.connect(self.selectAll) | |
|
159 | self._context_menu.addAction(select_all_action) | |
|
160 | ||
|
161 | def contextMenuEvent(self, event): | |
|
162 | """ Reimplemented to create a menu without destructive actions like | |
|
163 | 'Cut' and 'Delete'. | |
|
164 | """ | |
|
165 | clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty() | |
|
166 | self._paste_action.setEnabled(not clipboard_empty) | |
|
167 | ||
|
168 | self._context_menu.exec_(event.globalPos()) | |
|
169 | ||
|
170 | def keyPressEvent(self, event): | |
|
171 | """ Reimplemented to create a console-like interface. | |
|
172 | """ | |
|
173 | intercepted = False | |
|
174 | cursor = self.textCursor() | |
|
175 | position = cursor.position() | |
|
176 | key = event.key() | |
|
177 | ctrl_down = event.modifiers() & QtCore.Qt.ControlModifier | |
|
178 | alt_down = event.modifiers() & QtCore.Qt.AltModifier | |
|
179 | shift_down = event.modifiers() & QtCore.Qt.ShiftModifier | |
|
180 | ||
|
181 | if ctrl_down: | |
|
182 | if key in self._ctrl_down_remap: | |
|
183 | ctrl_down = False | |
|
184 | key = self._ctrl_down_remap[key] | |
|
185 | event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key, | |
|
186 | QtCore.Qt.NoModifier) | |
|
187 | ||
|
188 | elif key == QtCore.Qt.Key_K: | |
|
189 | if self._in_buffer(position): | |
|
190 | cursor.movePosition(QtGui.QTextCursor.EndOfLine, | |
|
191 | QtGui.QTextCursor.KeepAnchor) | |
|
192 | cursor.removeSelectedText() | |
|
193 | intercepted = True | |
|
194 | ||
|
195 | elif key == QtCore.Qt.Key_Y: | |
|
196 | self.paste() | |
|
197 | intercepted = True | |
|
198 | ||
|
199 | elif alt_down: | |
|
200 | if key == QtCore.Qt.Key_B: | |
|
201 | self.setTextCursor(self._get_word_start_cursor(position)) | |
|
202 | intercepted = True | |
|
203 | ||
|
204 | elif key == QtCore.Qt.Key_F: | |
|
205 | self.setTextCursor(self._get_word_end_cursor(position)) | |
|
206 | intercepted = True | |
|
207 | ||
|
208 | elif key == QtCore.Qt.Key_Backspace: | |
|
209 | cursor = self._get_word_start_cursor(position) | |
|
210 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) | |
|
211 | cursor.removeSelectedText() | |
|
212 | intercepted = True | |
|
213 | ||
|
214 | elif key == QtCore.Qt.Key_D: | |
|
215 | cursor = self._get_word_end_cursor(position) | |
|
216 | cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor) | |
|
217 | cursor.removeSelectedText() | |
|
218 | intercepted = True | |
|
219 | ||
|
220 | if self._completion_widget.isVisible(): | |
|
221 | self._completion_widget.keyPressEvent(event) | |
|
222 | intercepted = event.isAccepted() | |
|
223 | ||
|
224 | else: | |
|
225 | if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): | |
|
226 | if self._reading: | |
|
227 | self._reading = False | |
|
228 | elif not self._executing: | |
|
229 | self._executing = True | |
|
230 | self.execute(interactive=True) | |
|
231 | intercepted = True | |
|
232 | ||
|
233 | elif key == QtCore.Qt.Key_Up: | |
|
234 | if self._reading or not self._up_pressed(): | |
|
235 | intercepted = True | |
|
236 | else: | |
|
237 | prompt_line = self._get_prompt_cursor().blockNumber() | |
|
238 | intercepted = cursor.blockNumber() <= prompt_line | |
|
239 | ||
|
240 | elif key == QtCore.Qt.Key_Down: | |
|
241 | if self._reading or not self._down_pressed(): | |
|
242 | intercepted = True | |
|
243 | else: | |
|
244 | end_line = self._get_end_cursor().blockNumber() | |
|
245 | intercepted = cursor.blockNumber() == end_line | |
|
246 | ||
|
247 | elif key == QtCore.Qt.Key_Tab: | |
|
248 | if self._reading: | |
|
249 | intercepted = False | |
|
250 | else: | |
|
251 | intercepted = not self._tab_pressed() | |
|
252 | ||
|
253 | elif key == QtCore.Qt.Key_Left: | |
|
254 | intercepted = not self._in_buffer(position - 1) | |
|
255 | ||
|
256 | elif key == QtCore.Qt.Key_Home: | |
|
257 | cursor.movePosition(QtGui.QTextCursor.StartOfLine) | |
|
258 | start_pos = cursor.position() | |
|
259 | start_line = cursor.blockNumber() | |
|
260 | if start_line == self._get_prompt_cursor().blockNumber(): | |
|
261 | start_pos += len(self._prompt) | |
|
262 | else: | |
|
263 | start_pos += len(self.continuation_prompt) | |
|
264 | if shift_down and self._in_buffer(position): | |
|
265 | self._set_selection(position, start_pos) | |
|
266 | else: | |
|
267 | self._set_position(start_pos) | |
|
268 | intercepted = True | |
|
269 | ||
|
270 | elif key == QtCore.Qt.Key_Backspace and not alt_down: | |
|
271 | ||
|
272 | # Line deletion (remove continuation prompt) | |
|
273 | len_prompt = len(self.continuation_prompt) | |
|
274 | if cursor.columnNumber() == len_prompt and \ | |
|
275 | position != self._prompt_pos: | |
|
276 | cursor.setPosition(position - len_prompt, | |
|
277 | QtGui.QTextCursor.KeepAnchor) | |
|
278 | cursor.removeSelectedText() | |
|
279 | ||
|
280 | # Regular backwards deletion | |
|
281 | else: | |
|
282 | anchor = cursor.anchor() | |
|
283 | if anchor == position: | |
|
284 | intercepted = not self._in_buffer(position - 1) | |
|
285 | else: | |
|
286 | intercepted = not self._in_buffer(min(anchor, position)) | |
|
287 | ||
|
288 | elif key == QtCore.Qt.Key_Delete: | |
|
289 | anchor = cursor.anchor() | |
|
290 | intercepted = not self._in_buffer(min(anchor, position)) | |
|
291 | ||
|
292 | # Don't move cursor if control is down to allow copy-paste using | |
|
293 | # the keyboard in any part of the buffer | |
|
294 | if not ctrl_down: | |
|
295 | self._keep_cursor_in_buffer() | |
|
296 | ||
|
297 | if not intercepted: | |
|
298 | QtGui.QPlainTextEdit.keyPressEvent(self, event) | |
|
299 | ||
|
300 | #-------------------------------------------------------------------------- | |
|
301 | # 'QPlainTextEdit' interface | |
|
302 | #-------------------------------------------------------------------------- | |
|
303 | ||
|
304 | def appendPlainText(self, text): | |
|
305 | """ Reimplemented to not append text as a new paragraph, which doesn't | |
|
306 | make sense for a console widget. Also, if enabled, handle ANSI | |
|
307 | codes. | |
|
308 | """ | |
|
309 | cursor = self.textCursor() | |
|
310 | cursor.movePosition(QtGui.QTextCursor.End) | |
|
311 | ||
|
312 | if self.ansi_codes: | |
|
313 | format = QtGui.QTextCharFormat() | |
|
314 | previous_end = 0 | |
|
315 | for match in self._ansi_pattern.finditer(text): | |
|
316 | cursor.insertText(text[previous_end:match.start()], format) | |
|
317 | previous_end = match.end() | |
|
318 | for code in match.group(1).split(';'): | |
|
319 | self._ansi_processor.set_code(int(code)) | |
|
320 | format = self._ansi_processor.get_format() | |
|
321 | cursor.insertText(text[previous_end:], format) | |
|
322 | else: | |
|
323 | cursor.insertText(text) | |
|
324 | ||
|
325 | def paste(self): | |
|
326 | """ Reimplemented to ensure that text is pasted in the editing region. | |
|
327 | """ | |
|
328 | self._keep_cursor_in_buffer() | |
|
329 | QtGui.QPlainTextEdit.paste(self) | |
|
330 | ||
|
331 | #--------------------------------------------------------------------------- | |
|
332 | # 'ConsoleWidget' public interface | |
|
333 | #--------------------------------------------------------------------------- | |
|
334 | ||
|
335 | def execute(self, interactive=False): | |
|
336 | """ Execute the text in the input buffer. Returns whether the input | |
|
337 | buffer was completely processed and a new prompt created. | |
|
338 | """ | |
|
339 | self.appendPlainText('\n') | |
|
340 | self._prompt_finished() | |
|
341 | return self._execute(interactive=interactive) | |
|
342 | ||
|
343 | def _get_input_buffer(self): | |
|
344 | cursor = self._get_end_cursor() | |
|
345 | cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) | |
|
346 | ||
|
347 | # Use QTextDocumentFragment intermediate object because it strips | |
|
348 | # out the Unicode line break characters that Qt insists on inserting. | |
|
349 | input_buffer = str(cursor.selection().toPlainText()) | |
|
350 | ||
|
351 | # Strip out continuation prompts | |
|
352 | return input_buffer.replace('\n' + self.continuation_prompt, '\n') | |
|
353 | ||
|
354 | def _set_input_buffer(self, string): | |
|
355 | # Add continuation prompts where necessary | |
|
356 | lines = string.splitlines() | |
|
357 | for i in xrange(1, len(lines)): | |
|
358 | lines[i] = self.continuation_prompt + lines[i] | |
|
359 | string = '\n'.join(lines) | |
|
360 | ||
|
361 | # Replace buffer with new text | |
|
362 | cursor = self._get_end_cursor() | |
|
363 | cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor) | |
|
364 | cursor.insertText(string) | |
|
365 | self.moveCursor(QtGui.QTextCursor.End) | |
|
366 | ||
|
367 | input_buffer = property(_get_input_buffer, _set_input_buffer) | |
|
368 | ||
|
369 | def _get_input_buffer_cursor_line(self): | |
|
370 | cursor = self.textCursor() | |
|
371 | if cursor.position() >= self._prompt_pos: | |
|
372 | text = str(cursor.block().text()) | |
|
373 | if cursor.blockNumber() == self._get_prompt_cursor().blockNumber(): | |
|
374 | return text[len(self._prompt):] | |
|
375 | else: | |
|
376 | return text[len(self.continuation_prompt):] | |
|
377 | else: | |
|
378 | return None | |
|
379 | ||
|
380 | input_buffer_cursor_line = property(_get_input_buffer_cursor_line) | |
|
381 | ||
|
382 | #--------------------------------------------------------------------------- | |
|
383 | # 'ConsoleWidget' abstract interface | |
|
384 | #--------------------------------------------------------------------------- | |
|
385 | ||
|
386 | def _execute(self, interactive): | |
|
387 | """ Called to execute the input buffer. When triggered by an the enter | |
|
388 | key press, 'interactive' is True; otherwise, it is False. Returns | |
|
389 | whether the input buffer was completely processed and a new prompt | |
|
390 | created. | |
|
391 | """ | |
|
392 | raise NotImplementedError | |
|
393 | ||
|
394 | def _prompt_started_hook(self): | |
|
395 | """ Called immediately after a new prompt is displayed. | |
|
396 | """ | |
|
397 | pass | |
|
398 | ||
|
399 | def _prompt_finished_hook(self): | |
|
400 | """ Called immediately after a prompt is finished, i.e. when some input | |
|
401 | will be processed and a new prompt displayed. | |
|
402 | """ | |
|
403 | pass | |
|
404 | ||
|
405 | def _up_pressed(self): | |
|
406 | """ Called when the up key is pressed. Returns whether to continue | |
|
407 | processing the event. | |
|
408 | """ | |
|
409 | return True | |
|
410 | ||
|
411 | def _down_pressed(self): | |
|
412 | """ Called when the down key is pressed. Returns whether to continue | |
|
413 | processing the event. | |
|
414 | """ | |
|
415 | return True | |
|
416 | ||
|
417 | def _tab_pressed(self): | |
|
418 | """ Called when the tab key is pressed. Returns whether to continue | |
|
419 | processing the event. | |
|
420 | """ | |
|
421 | return False | |
|
422 | ||
|
423 | #-------------------------------------------------------------------------- | |
|
424 | # 'ConsoleWidget' protected interface | |
|
425 | #-------------------------------------------------------------------------- | |
|
426 | ||
|
427 | def _complete_with_items(self, cursor, items): | |
|
428 | """ Performs completion with 'items' at the specified cursor location. | |
|
429 | """ | |
|
430 | if len(items) == 1: | |
|
431 | cursor.setPosition(self.textCursor().position(), | |
|
432 | QtGui.QTextCursor.KeepAnchor) | |
|
433 | cursor.insertText(items[0]) | |
|
434 | elif len(items) > 1: | |
|
435 | if self.gui_completion: | |
|
436 | self._completion_widget.show_items(cursor, items) | |
|
437 | else: | |
|
438 | text = '\n'.join(items) + '\n' | |
|
439 | self._write_text_keeping_prompt(text) | |
|
440 | ||
|
441 | def _get_end_cursor(self): | |
|
442 | """ Convenience method that returns a cursor for the last character. | |
|
443 | """ | |
|
444 | cursor = self.textCursor() | |
|
445 | cursor.movePosition(QtGui.QTextCursor.End) | |
|
446 | return cursor | |
|
447 | ||
|
448 | def _get_prompt_cursor(self): | |
|
449 | """ Convenience method that returns a cursor for the prompt position. | |
|
450 | """ | |
|
451 | cursor = self.textCursor() | |
|
452 | cursor.setPosition(self._prompt_pos) | |
|
453 | return cursor | |
|
454 | ||
|
455 | def _get_selection_cursor(self, start, end): | |
|
456 | """ Convenience method that returns a cursor with text selected between | |
|
457 | the positions 'start' and 'end'. | |
|
458 | """ | |
|
459 | cursor = self.textCursor() | |
|
460 | cursor.setPosition(start) | |
|
461 | cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor) | |
|
462 | return cursor | |
|
463 | ||
|
464 | def _get_word_start_cursor(self, position): | |
|
465 | """ Find the start of the word to the left the given position. If a | |
|
466 | sequence of non-word characters precedes the first word, skip over | |
|
467 | them. (This emulates the behavior of bash, emacs, etc.) | |
|
468 | """ | |
|
469 | document = self.document() | |
|
470 | position -= 1 | |
|
471 | while self._in_buffer(position) and \ | |
|
472 | not document.characterAt(position).isLetterOrNumber(): | |
|
473 | position -= 1 | |
|
474 | while self._in_buffer(position) and \ | |
|
475 | document.characterAt(position).isLetterOrNumber(): | |
|
476 | position -= 1 | |
|
477 | cursor = self.textCursor() | |
|
478 | cursor.setPosition(position + 1) | |
|
479 | return cursor | |
|
480 | ||
|
481 | def _get_word_end_cursor(self, position): | |
|
482 | """ Find the end of the word to the right the given position. If a | |
|
483 | sequence of non-word characters precedes the first word, skip over | |
|
484 | them. (This emulates the behavior of bash, emacs, etc.) | |
|
485 | """ | |
|
486 | document = self.document() | |
|
487 | end = self._get_end_cursor().position() | |
|
488 | while position < end and \ | |
|
489 | not document.characterAt(position).isLetterOrNumber(): | |
|
490 | position += 1 | |
|
491 | while position < end and \ | |
|
492 | document.characterAt(position).isLetterOrNumber(): | |
|
493 | position += 1 | |
|
494 | cursor = self.textCursor() | |
|
495 | cursor.setPosition(position) | |
|
496 | return cursor | |
|
497 | ||
|
498 | def _prompt_started(self): | |
|
499 | """ Called immediately after a new prompt is displayed. | |
|
500 | """ | |
|
501 | self.moveCursor(QtGui.QTextCursor.End) | |
|
502 | self.centerCursor() | |
|
503 | self.setReadOnly(False) | |
|
504 | self._executing = False | |
|
505 | self._prompt_started_hook() | |
|
506 | ||
|
507 | def _prompt_finished(self): | |
|
508 | """ Called immediately after a prompt is finished, i.e. when some input | |
|
509 | will be processed and a new prompt displayed. | |
|
510 | """ | |
|
511 | self.setReadOnly(True) | |
|
512 | self._prompt_finished_hook() | |
|
513 | ||
|
514 | def _set_position(self, position): | |
|
515 | """ Convenience method to set the position of the cursor. | |
|
516 | """ | |
|
517 | cursor = self.textCursor() | |
|
518 | cursor.setPosition(position) | |
|
519 | self.setTextCursor(cursor) | |
|
520 | ||
|
521 | def _set_selection(self, start, end): | |
|
522 | """ Convenience method to set the current selected text. | |
|
523 | """ | |
|
524 | self.setTextCursor(self._get_selection_cursor(start, end)) | |
|
525 | ||
|
526 | def _show_prompt(self, prompt): | |
|
527 | """ Writes a new prompt at the end of the buffer. | |
|
528 | """ | |
|
529 | self.appendPlainText('\n' + prompt) | |
|
530 | self._prompt = prompt | |
|
531 | self._prompt_pos = self._get_end_cursor().position() | |
|
532 | self._prompt_started() | |
|
533 | ||
|
534 | def _show_continuation_prompt(self): | |
|
535 | """ Writes a new continuation prompt at the end of the buffer. | |
|
536 | """ | |
|
537 | self.appendPlainText(self.continuation_prompt) | |
|
538 | self._prompt_started() | |
|
539 | ||
|
540 | def _write_text_keeping_prompt(self, text): | |
|
541 | """ Writes 'text' after the current prompt, then restores the old prompt | |
|
542 | with its old input buffer. | |
|
543 | """ | |
|
544 | input_buffer = self.input_buffer | |
|
545 | self.appendPlainText('\n') | |
|
546 | self._prompt_finished() | |
|
547 | ||
|
548 | self.appendPlainText(text) | |
|
549 | self._show_prompt(self._prompt) | |
|
550 | self.input_buffer = input_buffer | |
|
551 | ||
|
552 | def _in_buffer(self, position): | |
|
553 | """ Returns whether the given position is inside the editing region. | |
|
554 | """ | |
|
555 | return position >= self._prompt_pos | |
|
556 | ||
|
557 | def _keep_cursor_in_buffer(self): | |
|
558 | """ Ensures that the cursor is inside the editing region. Returns | |
|
559 | whether the cursor was moved. | |
|
560 | """ | |
|
561 | cursor = self.textCursor() | |
|
562 | if cursor.position() < self._prompt_pos: | |
|
563 | cursor.movePosition(QtGui.QTextCursor.End) | |
|
564 | self.setTextCursor(cursor) | |
|
565 | return True | |
|
566 | else: | |
|
567 | return False | |
|
568 | ||
|
569 | ||
|
570 | class HistoryConsoleWidget(ConsoleWidget): | |
|
571 | """ A ConsoleWidget that keeps a history of the commands that have been | |
|
572 | executed. | |
|
573 | """ | |
|
574 | ||
|
575 | #--------------------------------------------------------------------------- | |
|
576 | # 'QWidget' interface | |
|
577 | #--------------------------------------------------------------------------- | |
|
578 | ||
|
579 | def __init__(self, parent=None): | |
|
580 | super(HistoryConsoleWidget, self).__init__(parent) | |
|
581 | ||
|
582 | self._history = [] | |
|
583 | self._history_index = 0 | |
|
584 | ||
|
585 | #--------------------------------------------------------------------------- | |
|
586 | # 'ConsoleWidget' public interface | |
|
587 | #--------------------------------------------------------------------------- | |
|
588 | ||
|
589 | def execute(self, interactive=False): | |
|
590 | """ Reimplemented to the store history. | |
|
591 | """ | |
|
592 | stripped = self.input_buffer.rstrip() | |
|
593 | executed = super(HistoryConsoleWidget, self).execute(interactive) | |
|
594 | if executed: | |
|
595 | self._history.append(stripped) | |
|
596 | self._history_index = len(self._history) | |
|
597 | return executed | |
|
598 | ||
|
599 | #--------------------------------------------------------------------------- | |
|
600 | # 'ConsoleWidget' abstract interface | |
|
601 | #--------------------------------------------------------------------------- | |
|
602 | ||
|
603 | def _up_pressed(self): | |
|
604 | """ Called when the up key is pressed. Returns whether to continue | |
|
605 | processing the event. | |
|
606 | """ | |
|
607 | prompt_cursor = self._get_prompt_cursor() | |
|
608 | if self.textCursor().blockNumber() == prompt_cursor.blockNumber(): | |
|
609 | self.history_previous() | |
|
610 | ||
|
611 | # Go to the first line of prompt for seemless history scrolling. | |
|
612 | cursor = self._get_prompt_cursor() | |
|
613 | cursor.movePosition(QtGui.QTextCursor.EndOfLine) | |
|
614 | self.setTextCursor(cursor) | |
|
615 | ||
|
616 | return False | |
|
617 | return True | |
|
618 | ||
|
619 | def _down_pressed(self): | |
|
620 | """ Called when the down key is pressed. Returns whether to continue | |
|
621 | processing the event. | |
|
622 | """ | |
|
623 | end_cursor = self._get_end_cursor() | |
|
624 | if self.textCursor().blockNumber() == end_cursor.blockNumber(): | |
|
625 | self.history_next() | |
|
626 | return False | |
|
627 | return True | |
|
628 | ||
|
629 | #--------------------------------------------------------------------------- | |
|
630 | # 'HistoryConsoleWidget' interface | |
|
631 | #--------------------------------------------------------------------------- | |
|
632 | ||
|
633 | def history_previous(self): | |
|
634 | """ If possible, set the input buffer to the previous item in the | |
|
635 | history. | |
|
636 | """ | |
|
637 | if self._history_index > 0: | |
|
638 | self._history_index -= 1 | |
|
639 | self.input_buffer = self._history[self._history_index] | |
|
640 | ||
|
641 | def history_next(self): | |
|
642 | """ Set the input buffer to the next item in the history, or a blank | |
|
643 | line if there is no subsequent item. | |
|
644 | """ | |
|
645 | if self._history_index < len(self._history): | |
|
646 | self._history_index += 1 | |
|
647 | if self._history_index < len(self._history): | |
|
648 | self.input_buffer = self._history[self._history_index] | |
|
649 | else: | |
|
650 | self.input_buffer = '' |
@@ -0,0 +1,383 b'' | |||
|
1 | # Standard library imports | |
|
2 | from codeop import CommandCompiler | |
|
3 | from threading import Thread | |
|
4 | import time | |
|
5 | import types | |
|
6 | ||
|
7 | # System library imports | |
|
8 | from IPython.zmq.session import Message, Session | |
|
9 | from pygments.lexers import PythonLexer | |
|
10 | from PyQt4 import QtCore, QtGui | |
|
11 | import zmq | |
|
12 | ||
|
13 | # ETS imports | |
|
14 | from enthought.pyface.ui.qt4.code_editor.pygments_highlighter import \ | |
|
15 | PygmentsHighlighter | |
|
16 | ||
|
17 | # Local imports | |
|
18 | from call_tip_widget import CallTipWidget | |
|
19 | from completion_lexer import CompletionLexer | |
|
20 | from console_widget import HistoryConsoleWidget | |
|
21 | ||
|
22 | ||
|
23 | class FrontendReplyThread(Thread, QtCore.QObject): | |
|
24 | """ A Thread that receives a reply from the kernel for the frontend. | |
|
25 | """ | |
|
26 | ||
|
27 | finished = QtCore.pyqtSignal() | |
|
28 | output_received = QtCore.pyqtSignal(Message) | |
|
29 | reply_received = QtCore.pyqtSignal(Message) | |
|
30 | ||
|
31 | def __init__(self, parent): | |
|
32 | """ Create a FrontendReplyThread for the specified frontend. | |
|
33 | """ | |
|
34 | assert isinstance(parent, FrontendWidget) | |
|
35 | QtCore.QObject.__init__(self, parent) | |
|
36 | Thread.__init__(self) | |
|
37 | ||
|
38 | self.sleep_time = 0.05 | |
|
39 | ||
|
40 | def run(self): | |
|
41 | """ The starting point for the thread. | |
|
42 | """ | |
|
43 | frontend = self.parent() | |
|
44 | while True: | |
|
45 | rep = frontend._recv_reply() | |
|
46 | if rep is not None: | |
|
47 | self._recv_output() | |
|
48 | self.reply_received.emit(rep) | |
|
49 | break | |
|
50 | ||
|
51 | self._recv_output() | |
|
52 | time.sleep(self.sleep_time) | |
|
53 | ||
|
54 | self.finished.emit() | |
|
55 | ||
|
56 | def _recv_output(self): | |
|
57 | """ Send any output to the frontend. | |
|
58 | """ | |
|
59 | frontend = self.parent() | |
|
60 | omsgs = frontend._recv_output() | |
|
61 | for omsg in omsgs: | |
|
62 | self.output_received.emit(omsg) | |
|
63 | ||
|
64 | ||
|
65 | class FrontendHighlighter(PygmentsHighlighter): | |
|
66 | """ A Python PygmentsHighlighter that can be turned on and off and which | |
|
67 | knows about continuation prompts. | |
|
68 | """ | |
|
69 | ||
|
70 | def __init__(self, frontend): | |
|
71 | PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer()) | |
|
72 | self._current_offset = 0 | |
|
73 | self._frontend = frontend | |
|
74 | self.highlighting_on = False | |
|
75 | ||
|
76 | def highlightBlock(self, qstring): | |
|
77 | """ Highlight a block of text. Reimplemented to highlight selectively. | |
|
78 | """ | |
|
79 | if self.highlighting_on: | |
|
80 | for prompt in (self._frontend._prompt, | |
|
81 | self._frontend.continuation_prompt): | |
|
82 | if qstring.startsWith(prompt): | |
|
83 | qstring.remove(0, len(prompt)) | |
|
84 | self._current_offset = len(prompt) | |
|
85 | break | |
|
86 | PygmentsHighlighter.highlightBlock(self, qstring) | |
|
87 | ||
|
88 | def setFormat(self, start, count, format): | |
|
89 | """ Reimplemented to avoid highlighting continuation prompts. | |
|
90 | """ | |
|
91 | start += self._current_offset | |
|
92 | PygmentsHighlighter.setFormat(self, start, count, format) | |
|
93 | ||
|
94 | ||
|
95 | class FrontendWidget(HistoryConsoleWidget): | |
|
96 | """ A Qt frontend for an IPython kernel. | |
|
97 | """ | |
|
98 | ||
|
99 | # Emitted when an 'execute_reply' is received from the kernel. | |
|
100 | executed = QtCore.pyqtSignal(Message) | |
|
101 | ||
|
102 | #--------------------------------------------------------------------------- | |
|
103 | # 'QWidget' interface | |
|
104 | #--------------------------------------------------------------------------- | |
|
105 | ||
|
106 | def __init__(self, parent=None, session=None, request_socket=None, | |
|
107 | sub_socket=None): | |
|
108 | super(FrontendWidget, self).__init__(parent) | |
|
109 | self.continuation_prompt = '... ' | |
|
110 | ||
|
111 | self._call_tip_widget = CallTipWidget(self) | |
|
112 | self._compile = CommandCompiler() | |
|
113 | self._completion_lexer = CompletionLexer(PythonLexer()) | |
|
114 | self._highlighter = FrontendHighlighter(self) | |
|
115 | ||
|
116 | self.session = Session() if session is None else session | |
|
117 | self.request_socket = request_socket | |
|
118 | self.sub_socket = sub_socket | |
|
119 | ||
|
120 | self.document().contentsChange.connect(self._document_contents_change) | |
|
121 | ||
|
122 | self._kernel_connected() # XXX | |
|
123 | ||
|
124 | def focusOutEvent(self, event): | |
|
125 | """ Reimplemented to hide calltips. | |
|
126 | """ | |
|
127 | self._call_tip_widget.hide() | |
|
128 | return super(FrontendWidget, self).focusOutEvent(event) | |
|
129 | ||
|
130 | def keyPressEvent(self, event): | |
|
131 | """ Reimplemented to hide calltips. | |
|
132 | """ | |
|
133 | if event.key() == QtCore.Qt.Key_Escape: | |
|
134 | self._call_tip_widget.hide() | |
|
135 | return super(FrontendWidget, self).keyPressEvent(event) | |
|
136 | ||
|
137 | #--------------------------------------------------------------------------- | |
|
138 | # 'ConsoleWidget' abstract interface | |
|
139 | #--------------------------------------------------------------------------- | |
|
140 | ||
|
141 | def _execute(self, interactive): | |
|
142 | """ Called to execute the input buffer. When triggered by an the enter | |
|
143 | key press, 'interactive' is True; otherwise, it is False. Returns | |
|
144 | whether the input buffer was completely processed and a new prompt | |
|
145 | created. | |
|
146 | """ | |
|
147 | return self.execute_source(self.input_buffer, interactive=interactive) | |
|
148 | ||
|
149 | def _prompt_started_hook(self): | |
|
150 | """ Called immediately after a new prompt is displayed. | |
|
151 | """ | |
|
152 | self._highlighter.highlighting_on = True | |
|
153 | ||
|
154 | def _prompt_finished_hook(self): | |
|
155 | """ Called immediately after a prompt is finished, i.e. when some input | |
|
156 | will be processed and a new prompt displayed. | |
|
157 | """ | |
|
158 | self._highlighter.highlighting_on = False | |
|
159 | ||
|
160 | def _tab_pressed(self): | |
|
161 | """ Called when the tab key is pressed. Returns whether to continue | |
|
162 | processing the event. | |
|
163 | """ | |
|
164 | self._keep_cursor_in_buffer() | |
|
165 | cursor = self.textCursor() | |
|
166 | if not self._complete(): | |
|
167 | cursor.insertText(' ') | |
|
168 | return False | |
|
169 | ||
|
170 | #--------------------------------------------------------------------------- | |
|
171 | # 'FrontendWidget' interface | |
|
172 | #--------------------------------------------------------------------------- | |
|
173 | ||
|
174 | def execute_source(self, source, hidden=False, interactive=False): | |
|
175 | """ Execute a string containing Python code. If 'hidden', no output is | |
|
176 | shown. Returns whether the source executed (i.e., returns True only | |
|
177 | if no more input is necessary). | |
|
178 | """ | |
|
179 | try: | |
|
180 | code = self._compile(source, symbol='single') | |
|
181 | except (OverflowError, SyntaxError, ValueError): | |
|
182 | # Just let IPython deal with the syntax error. | |
|
183 | code = Exception | |
|
184 | ||
|
185 | # Only execute interactive multiline input if it ends with a blank line | |
|
186 | lines = source.splitlines() | |
|
187 | if interactive and len(lines) > 1 and lines[-1].strip() != '': | |
|
188 | code = None | |
|
189 | ||
|
190 | executed = code is not None | |
|
191 | if executed: | |
|
192 | msg = self.session.send(self.request_socket, 'execute_request', | |
|
193 | dict(code=source)) | |
|
194 | thread = FrontendReplyThread(self) | |
|
195 | if not hidden: | |
|
196 | thread.output_received.connect(self._handle_output) | |
|
197 | thread.reply_received.connect(self._handle_reply) | |
|
198 | thread.finished.connect(thread.deleteLater) | |
|
199 | thread.start() | |
|
200 | else: | |
|
201 | space = 0 | |
|
202 | for char in lines[-1]: | |
|
203 | if char == '\t': | |
|
204 | space += 4 | |
|
205 | elif char == ' ': | |
|
206 | space += 1 | |
|
207 | else: | |
|
208 | break | |
|
209 | if source.endswith(':') or source.endswith(':\n'): | |
|
210 | space += 4 | |
|
211 | self._show_continuation_prompt() | |
|
212 | self.appendPlainText(' ' * space) | |
|
213 | ||
|
214 | return executed | |
|
215 | ||
|
216 | def execute_file(self, path, hidden=False): | |
|
217 | """ Attempts to execute file with 'path'. If 'hidden', no output is | |
|
218 | shown. | |
|
219 | """ | |
|
220 | self.execute_source('run %s' % path, hidden=hidden) | |
|
221 | ||
|
222 | #--------------------------------------------------------------------------- | |
|
223 | # 'FrontendWidget' protected interface | |
|
224 | #--------------------------------------------------------------------------- | |
|
225 | ||
|
226 | def _call_tip(self): | |
|
227 | """ Shows a call tip, if appropriate, at the current cursor location. | |
|
228 | """ | |
|
229 | # Decide if it makes sense to show a call tip | |
|
230 | cursor = self.textCursor() | |
|
231 | cursor.movePosition(QtGui.QTextCursor.Left) | |
|
232 | document = self.document() | |
|
233 | if document.characterAt(cursor.position()).toAscii() != '(': | |
|
234 | return False | |
|
235 | context = self._get_context(cursor) | |
|
236 | if not context: | |
|
237 | return False | |
|
238 | ||
|
239 | # Send the metadata request to the kernel | |
|
240 | text = '.'.join(context) | |
|
241 | msg = self.session.send(self.request_socket, 'metadata_request', | |
|
242 | dict(context=text)) | |
|
243 | ||
|
244 | # Give the kernel some time to respond | |
|
245 | rep = self._recv_reply_now('metadata_reply') | |
|
246 | doc = rep.content.docstring if rep else '' | |
|
247 | ||
|
248 | # Show the call tip | |
|
249 | if doc: | |
|
250 | self._call_tip_widget.show_tip(doc) | |
|
251 | return True | |
|
252 | ||
|
253 | def _complete(self): | |
|
254 | """ Performs completion at the current cursor location. | |
|
255 | """ | |
|
256 | # Decide if it makes sense to do completion | |
|
257 | context = self._get_context() | |
|
258 | if not context: | |
|
259 | return False | |
|
260 | ||
|
261 | # Send the completion request to the kernel | |
|
262 | text = '.'.join(context) | |
|
263 | line = self.input_buffer_cursor_line | |
|
264 | msg = self.session.send(self.request_socket, 'complete_request', | |
|
265 | dict(text=text, line=line)) | |
|
266 | ||
|
267 | # Give the kernel some time to respond | |
|
268 | rep = self._recv_reply_now('complete_reply') | |
|
269 | matches = rep.content.matches if rep else [] | |
|
270 | ||
|
271 | # Show the completion at the correct location | |
|
272 | cursor = self.textCursor() | |
|
273 | cursor.movePosition(QtGui.QTextCursor.Left, n=len(text)) | |
|
274 | self._complete_with_items(cursor, matches) | |
|
275 | return True | |
|
276 | ||
|
277 | def _kernel_connected(self): | |
|
278 | """ Called when the frontend is connected to a kernel. | |
|
279 | """ | |
|
280 | self._show_prompt('>>> ') | |
|
281 | ||
|
282 | def _get_context(self, cursor=None): | |
|
283 | """ Gets the context at the current cursor location. | |
|
284 | """ | |
|
285 | if cursor is None: | |
|
286 | cursor = self.textCursor() | |
|
287 | cursor.movePosition(QtGui.QTextCursor.StartOfLine, | |
|
288 | QtGui.QTextCursor.KeepAnchor) | |
|
289 | text = unicode(cursor.selectedText()) | |
|
290 | return self._completion_lexer.get_context(text) | |
|
291 | ||
|
292 | #------ Signal handlers ---------------------------------------------------- | |
|
293 | ||
|
294 | def _document_contents_change(self, position, removed, added): | |
|
295 | """ Called whenever the document's content changes. Display a calltip | |
|
296 | if appropriate. | |
|
297 | """ | |
|
298 | # Calculate where the cursor should be *after* the change: | |
|
299 | position += added | |
|
300 | ||
|
301 | document = self.document() | |
|
302 | if position == self.textCursor().position(): | |
|
303 | self._call_tip() | |
|
304 | ||
|
305 | def _handle_output(self, omsg): | |
|
306 | handler = getattr(self, '_handle_%s' % omsg.msg_type, None) | |
|
307 | if handler is not None: | |
|
308 | handler(omsg) | |
|
309 | ||
|
310 | def _handle_pyout(self, omsg): | |
|
311 | if omsg.parent_header.session == self.session.session: | |
|
312 | self.appendPlainText(omsg.content.data + '\n') | |
|
313 | ||
|
314 | def _handle_stream(self, omsg): | |
|
315 | self.appendPlainText(omsg.content.data) | |
|
316 | ||
|
317 | def _handle_reply(self, rep): | |
|
318 | if rep is not None: | |
|
319 | if rep.msg_type == 'execute_reply': | |
|
320 | if rep.content.status == 'error': | |
|
321 | self.appendPlainText(rep.content.traceback[-1]) | |
|
322 | elif rep.content.status == 'aborted': | |
|
323 | text = "ERROR: ABORTED\n" | |
|
324 | ab = self.messages[rep.parent_header.msg_id].content | |
|
325 | if 'code' in ab: | |
|
326 | text += ab.code | |
|
327 | else: | |
|
328 | text += ab | |
|
329 | self.appendPlainText(text) | |
|
330 | self._show_prompt('>>> ') | |
|
331 | self.executed.emit(rep) | |
|
332 | ||
|
333 | #------ Communication methods ---------------------------------------------- | |
|
334 | ||
|
335 | def _recv_output(self): | |
|
336 | omsgs = [] | |
|
337 | while True: | |
|
338 | omsg = self.session.recv(self.sub_socket) | |
|
339 | if omsg is None: | |
|
340 | break | |
|
341 | else: | |
|
342 | omsgs.append(omsg) | |
|
343 | return omsgs | |
|
344 | ||
|
345 | def _recv_reply(self): | |
|
346 | return self.session.recv(self.request_socket) | |
|
347 | ||
|
348 | def _recv_reply_now(self, msg_type): | |
|
349 | for i in xrange(5): | |
|
350 | rep = self._recv_reply() | |
|
351 | if rep is not None and rep.msg_type == msg_type: | |
|
352 | return rep | |
|
353 | time.sleep(0.1) | |
|
354 | return None | |
|
355 | ||
|
356 | ||
|
357 | if __name__ == '__main__': | |
|
358 | import sys | |
|
359 | ||
|
360 | # Defaults | |
|
361 | ip = '127.0.0.1' | |
|
362 | port_base = 5555 | |
|
363 | connection = ('tcp://%s' % ip) + ':%i' | |
|
364 | req_conn = connection % port_base | |
|
365 | sub_conn = connection % (port_base+1) | |
|
366 | ||
|
367 | # Create initial sockets | |
|
368 | c = zmq.Context() | |
|
369 | request_socket = c.socket(zmq.XREQ) | |
|
370 | request_socket.connect(req_conn) | |
|
371 | sub_socket = c.socket(zmq.SUB) | |
|
372 | sub_socket.connect(sub_conn) | |
|
373 | sub_socket.setsockopt(zmq.SUBSCRIBE, '') | |
|
374 | ||
|
375 | # Launch application | |
|
376 | app = QtGui.QApplication(sys.argv) | |
|
377 | widget = FrontendWidget(request_socket=request_socket, | |
|
378 | sub_socket=sub_socket) | |
|
379 | widget.setWindowTitle('Python') | |
|
380 | widget.resize(640, 480) | |
|
381 | widget.show() | |
|
382 | sys.exit(app.exec_()) | |
|
383 |
@@ -0,0 +1,221 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 | ||
|
12 | from PyQt4 import QtGui | |
|
13 | ||
|
14 | from pygments.lexer import RegexLexer, _TokenType, Text, Error | |
|
15 | from pygments.lexers import CLexer, CppLexer, PythonLexer | |
|
16 | from pygments.styles.default import DefaultStyle | |
|
17 | from pygments.token import Comment | |
|
18 | ||
|
19 | ||
|
20 | def get_tokens_unprocessed(self, text, stack=('root',)): | |
|
21 | """ Split ``text`` into (tokentype, text) pairs. | |
|
22 | ||
|
23 | Monkeypatched to store the final stack on the object itself. | |
|
24 | """ | |
|
25 | pos = 0 | |
|
26 | tokendefs = self._tokens | |
|
27 | if hasattr(self, '_epd_state_stack'): | |
|
28 | statestack = list(self._epd_state_stack) | |
|
29 | else: | |
|
30 | statestack = list(stack) | |
|
31 | statetokens = tokendefs[statestack[-1]] | |
|
32 | while 1: | |
|
33 | for rexmatch, action, new_state in statetokens: | |
|
34 | m = rexmatch(text, pos) | |
|
35 | if m: | |
|
36 | if type(action) is _TokenType: | |
|
37 | yield pos, action, m.group() | |
|
38 | else: | |
|
39 | for item in action(self, m): | |
|
40 | yield item | |
|
41 | pos = m.end() | |
|
42 | if new_state is not None: | |
|
43 | # state transition | |
|
44 | if isinstance(new_state, tuple): | |
|
45 | for state in new_state: | |
|
46 | if state == '#pop': | |
|
47 | statestack.pop() | |
|
48 | elif state == '#push': | |
|
49 | statestack.append(statestack[-1]) | |
|
50 | else: | |
|
51 | statestack.append(state) | |
|
52 | elif isinstance(new_state, int): | |
|
53 | # pop | |
|
54 | del statestack[new_state:] | |
|
55 | elif new_state == '#push': | |
|
56 | statestack.append(statestack[-1]) | |
|
57 | else: | |
|
58 | assert False, "wrong state def: %r" % new_state | |
|
59 | statetokens = tokendefs[statestack[-1]] | |
|
60 | break | |
|
61 | else: | |
|
62 | try: | |
|
63 | if text[pos] == '\n': | |
|
64 | # at EOL, reset state to "root" | |
|
65 | pos += 1 | |
|
66 | statestack = ['root'] | |
|
67 | statetokens = tokendefs['root'] | |
|
68 | yield pos, Text, u'\n' | |
|
69 | continue | |
|
70 | yield pos, Error, text[pos] | |
|
71 | pos += 1 | |
|
72 | except IndexError: | |
|
73 | break | |
|
74 | self._epd_state_stack = list(statestack) | |
|
75 | ||
|
76 | # Monkeypatch! | |
|
77 | RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed | |
|
78 | ||
|
79 | ||
|
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): | |
|
108 | """ Storage for the user data associated with each line. | |
|
109 | """ | |
|
110 | ||
|
111 | syntax_stack = ('root',) | |
|
112 | ||
|
113 | def __init__(self, **kwds): | |
|
114 | for key, value in kwds.iteritems(): | |
|
115 | setattr(self, key, value) | |
|
116 | QtGui.QTextBlockUserData.__init__(self) | |
|
117 | ||
|
118 | def __repr__(self): | |
|
119 | attrs = ['syntax_stack'] | |
|
120 | kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr)) | |
|
121 | for attr in attrs ]) | |
|
122 | return 'BlockUserData(%s)' % kwds | |
|
123 | ||
|
124 | ||
|
125 | class PygmentsHighlighter(QtGui.QSyntaxHighlighter): | |
|
126 | """ Syntax highlighter that uses Pygments for parsing. """ | |
|
127 | ||
|
128 | def __init__(self, parent, lexer=None): | |
|
129 | super(PygmentsHighlighter, self).__init__(parent) | |
|
130 | ||
|
131 | self._lexer = lexer if lexer else PythonLexer() | |
|
132 | self._style = DefaultStyle | |
|
133 | # Caches for formats and brushes. | |
|
134 | self._brushes = {} | |
|
135 | self._formats = {} | |
|
136 | ||
|
137 | def highlightBlock(self, qstring): | |
|
138 | """ Highlight a block of text. | |
|
139 | """ | |
|
140 | qstring = unicode(qstring) | |
|
141 | prev_data = self.previous_block_data() | |
|
142 | ||
|
143 | 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 | |
|
147 | ||
|
148 | index = 0 | |
|
149 | # Lex the text using Pygments | |
|
150 | for token, text in self._lexer.get_tokens(qstring): | |
|
151 | l = len(text) | |
|
152 | format = self._get_format(token) | |
|
153 | if format is not None: | |
|
154 | self.setFormat(index, l, format) | |
|
155 | index += l | |
|
156 | ||
|
157 | if hasattr(self._lexer, '_epd_state_stack'): | |
|
158 | data = BlockUserData(syntax_stack=self._lexer._epd_state_stack) | |
|
159 | self.currentBlock().setUserData(data) | |
|
160 | # Clean up for the next go-round. | |
|
161 | del self._lexer._epd_state_stack | |
|
162 | ||
|
163 | def previous_block_data(self): | |
|
164 | """ Convenience method for returning the previous block's user data. | |
|
165 | """ | |
|
166 | return self.currentBlock().previous().userData() | |
|
167 | ||
|
168 | def _get_format(self, token): | |
|
169 | """ Returns a QTextCharFormat for token or None. | |
|
170 | """ | |
|
171 | if token in self._formats: | |
|
172 | return self._formats[token] | |
|
173 | result = None | |
|
174 | for key, value in self._style.style_for_token(token) .items(): | |
|
175 | if value: | |
|
176 | if result is None: | |
|
177 | result = QtGui.QTextCharFormat() | |
|
178 | if key == 'color': | |
|
179 | result.setForeground(self._get_brush(value)) | |
|
180 | elif key == 'bgcolor': | |
|
181 | result.setBackground(self._get_brush(value)) | |
|
182 | elif key == 'bold': | |
|
183 | result.setFontWeight(QtGui.QFont.Bold) | |
|
184 | elif key == 'italic': | |
|
185 | result.setFontItalic(True) | |
|
186 | elif key == 'underline': | |
|
187 | result.setUnderlineStyle( | |
|
188 | QtGui.QTextCharFormat.SingleUnderline) | |
|
189 | elif key == 'sans': | |
|
190 | result.setFontStyleHint(QtGui.QFont.SansSerif) | |
|
191 | elif key == 'roman': | |
|
192 | result.setFontStyleHint(QtGui.QFont.Times) | |
|
193 | elif key == 'mono': | |
|
194 | result.setFontStyleHint(QtGui.QFont.TypeWriter) | |
|
195 | elif key == 'border': | |
|
196 | # Borders are normally used for errors. We can't do a border | |
|
197 | # so instead we do a wavy underline | |
|
198 | result.setUnderlineStyle( | |
|
199 | QtGui.QTextCharFormat.WaveUnderline) | |
|
200 | result.setUnderlineColor(self._get_color(value)) | |
|
201 | self._formats[token] = result | |
|
202 | return result | |
|
203 | ||
|
204 | def _get_brush(self, color): | |
|
205 | """ Returns a brush for the color. | |
|
206 | """ | |
|
207 | result = self._brushes.get(color) | |
|
208 | if result is None: | |
|
209 | qcolor = self._get_color(color) | |
|
210 | result = QtGui.QBrush(qcolor) | |
|
211 | self._brushes[color] = result | |
|
212 | ||
|
213 | return result | |
|
214 | ||
|
215 | def _get_color(self, color): | |
|
216 | qcolor = QtGui.QColor() | |
|
217 | qcolor.setRgb(int(color[:2],base=16), | |
|
218 | int(color[2:4], base=16), | |
|
219 | int(color[4:6], base=16)) | |
|
220 | return qcolor | |
|
221 |
General Comments 0
You need to be logged in to leave comments.
Login now