diff --git a/IPython/frontend/qt/console/call_tip_widget.py b/IPython/frontend/qt/console/call_tip_widget.py
new file mode 100644
index 0000000..b86c5a2
--- /dev/null
+++ b/IPython/frontend/qt/console/call_tip_widget.py
@@ -0,0 +1,156 @@
+# System library imports
+from PyQt4 import QtCore, QtGui
+
+
+class CallTipWidget(QtGui.QLabel):
+ """ Shows call tips by parsing the current text of Q[Plain]TextEdit.
+ """
+
+ #--------------------------------------------------------------------------
+ # 'QWidget' interface
+ #--------------------------------------------------------------------------
+
+ def __init__(self, parent):
+ """ Create a call tip manager that is attached to the specified Qt
+ text edit widget.
+ """
+ assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
+ QtGui.QLabel.__init__(self, parent, QtCore.Qt.ToolTip)
+
+ self.setFont(parent.document().defaultFont())
+ self.setForegroundRole(QtGui.QPalette.ToolTipText)
+ self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
+ self.setPalette(QtGui.QToolTip.palette())
+
+ self.setAlignment(QtCore.Qt.AlignLeft)
+ self.setIndent(1)
+ self.setFrameStyle(QtGui.QFrame.NoFrame)
+ self.setMargin(1 + self.style().pixelMetric(
+ QtGui.QStyle.PM_ToolTipLabelFrameWidth, None, self))
+ self.setWindowOpacity(self.style().styleHint(
+ QtGui.QStyle.SH_ToolTipLabel_Opacity, None, self) / 255.0)
+
+ def hideEvent(self, event):
+ """ Reimplemented to disconnect the cursor movement handler.
+ """
+ QtGui.QListWidget.hideEvent(self, event)
+ self.parent().cursorPositionChanged.disconnect(self._update_tip)
+
+ def paintEvent(self, event):
+ """ Reimplemented to paint the background panel.
+ """
+ painter = QtGui.QStylePainter(self)
+ option = QtGui.QStyleOptionFrame()
+ option.init(self)
+ painter.drawPrimitive(QtGui.QStyle.PE_PanelTipLabel, option)
+ painter.end()
+
+ QtGui.QLabel.paintEvent(self, event)
+
+ def showEvent(self, event):
+ """ Reimplemented to connect the cursor movement handler.
+ """
+ QtGui.QListWidget.showEvent(self, event)
+ self.parent().cursorPositionChanged.connect(self._update_tip)
+
+ #--------------------------------------------------------------------------
+ # 'CallTipWidget' interface
+ #--------------------------------------------------------------------------
+
+ def show_tip(self, tip):
+ """ Attempts to show the specified tip at the current cursor location.
+ """
+ text_edit = self.parent()
+ document = text_edit.document()
+ cursor = text_edit.textCursor()
+ search_pos = cursor.position() - 1
+ self._start_position, _ = self._find_parenthesis(search_pos,
+ forward=False)
+ if self._start_position == -1:
+ return False
+
+ point = text_edit.cursorRect(cursor).bottomRight()
+ point = text_edit.mapToGlobal(point)
+ self.move(point)
+ self.setText(tip)
+ if self.isVisible():
+ self.resize(self.sizeHint())
+ else:
+ self.show()
+ return True
+
+ #--------------------------------------------------------------------------
+ # Protected interface
+ #--------------------------------------------------------------------------
+
+ def _find_parenthesis(self, position, forward=True):
+ """ If 'forward' is True (resp. False), proceed forwards
+ (resp. backwards) through the line that contains 'position' until an
+ unmatched closing (resp. opening) parenthesis is found. Returns a
+ tuple containing the position of this parenthesis (or -1 if it is
+ not found) and the number commas (at depth 0) found along the way.
+ """
+ commas = depth = 0
+ document = self.parent().document()
+ qchar = document.characterAt(position)
+ while (position > 0 and qchar.isPrint() and
+ # Need to check explicitly for line/paragraph separators:
+ qchar.unicode() not in (0x2028, 0x2029)):
+ char = qchar.toAscii()
+ if char == ',' and depth == 0:
+ commas += 1
+ elif char == ')':
+ if forward and depth == 0:
+ break
+ depth += 1
+ elif char == '(':
+ if not forward and depth == 0:
+ break
+ depth -= 1
+ position += 1 if forward else -1
+ qchar = document.characterAt(position)
+ else:
+ position = -1
+ return position, commas
+
+ def _highlight_tip(self, tip, current_argument):
+ """ Highlight the current argument (arguments start at 0), ending at the
+ next comma or unmatched closing parenthesis.
+
+ FIXME: This is an unreliable way to do things and it isn't being
+ used right now. Instead, we should use inspect.getargspec
+ metadata for this purpose.
+ """
+ start = tip.find('(')
+ if start != -1:
+ for i in xrange(current_argument):
+ start = tip.find(',', start)
+ if start != -1:
+ end = start + 1
+ while end < len(tip):
+ char = tip[end]
+ depth = 0
+ if (char == ',' and depth == 0):
+ break
+ elif char == '(':
+ depth += 1
+ elif char == ')':
+ if depth == 0:
+ break
+ depth -= 1
+ end += 1
+ tip = tip[:start+1] + '' + \
+ tip[start+1:end] + '' + tip[end:]
+ tip = tip.replace('\n', '
')
+ return tip
+
+ def _update_tip(self):
+ """ Updates the tip based on user cursor movement.
+ """
+ cursor = self.parent().textCursor()
+ if cursor.position() <= self._start_position:
+ self.hide()
+ else:
+ position, commas = self._find_parenthesis(self._start_position + 1)
+ if position != -1:
+ self.hide()
diff --git a/IPython/frontend/qt/console/completion_lexer.py b/IPython/frontend/qt/console/completion_lexer.py
new file mode 100644
index 0000000..5eed553
--- /dev/null
+++ b/IPython/frontend/qt/console/completion_lexer.py
@@ -0,0 +1,57 @@
+# System library imports
+from pygments.token import Token, is_token_subtype
+
+
+class CompletionLexer(object):
+ """ Uses Pygments and some auxillary information to lex code snippets for
+ symbol contexts.
+ """
+
+ # Maps Lexer names to a list of possible name separators
+ separator_map = { 'C' : [ '.', '->' ],
+ 'C++' : [ '.', '->', '::' ],
+ 'Python' : [ '.' ] }
+
+ def __init__(self, lexer):
+ self.lexer = lexer
+
+ def get_context(self, string):
+ """ Assuming the cursor is at the end of the specified string, get the
+ context (a list of names) for the symbol at cursor position.
+ """
+ context = []
+ reversed_tokens = list(self._lexer.get_tokens(string))
+ reversed_tokens.reverse()
+
+ # Pygments often tacks on a newline when none is specified in the input
+ if reversed_tokens and reversed_tokens[0][1].endswith('\n') and \
+ not string.endswith('\n'):
+ reversed_tokens.pop(0)
+
+ current_op = unicode()
+ for token, text in reversed_tokens:
+ if is_token_subtype(token, Token.Name) and \
+ (not context or current_op in self._name_separators):
+ if not context and current_op in self._name_separators:
+ context.insert(0, unicode())
+ context.insert(0, text)
+ current_op = unicode()
+ elif token is Token.Operator or token is Token.Punctuation:
+ current_op = text + current_op
+ else:
+ break
+
+ return context
+
+ def get_lexer(self, lexer):
+ return self._lexer
+
+ def set_lexer(self, lexer, name_separators=None):
+ self._lexer = lexer
+ if name_separators is None:
+ self._name_separators = self.separator_map.get(lexer.name, ['.'])
+ else:
+ self._name_separators = list(name_separators)
+
+ lexer = property(get_lexer, set_lexer)
+
diff --git a/IPython/frontend/qt/console/completion_widget.py b/IPython/frontend/qt/console/completion_widget.py
new file mode 100644
index 0000000..4290423
--- /dev/null
+++ b/IPython/frontend/qt/console/completion_widget.py
@@ -0,0 +1,121 @@
+# System library imports
+from PyQt4 import QtCore, QtGui
+
+
+class CompletionWidget(QtGui.QListWidget):
+ """ A widget for GUI tab completion.
+ """
+
+ #--------------------------------------------------------------------------
+ # 'QWidget' interface
+ #--------------------------------------------------------------------------
+
+ def __init__(self, parent):
+ """ Create a completion widget that is attached to the specified Qt
+ text edit widget.
+ """
+ assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
+ QtGui.QListWidget.__init__(self, parent)
+
+ self.setWindowFlags(QtCore.Qt.ToolTip | QtCore.Qt.WindowStaysOnTopHint)
+ self.setAttribute(QtCore.Qt.WA_StaticContents)
+
+ # Ensure that parent keeps focus when widget is displayed.
+ self.setFocusProxy(parent)
+
+ self.setFrameShadow(QtGui.QFrame.Plain)
+ self.setFrameShape(QtGui.QFrame.StyledPanel)
+
+ self.itemActivated.connect(self._complete_current)
+
+ def hideEvent(self, event):
+ """ Reimplemented to disconnect the cursor movement handler.
+ """
+ QtGui.QListWidget.hideEvent(self, event)
+ self.parent().cursorPositionChanged.disconnect(self._update_current)
+
+ def keyPressEvent(self, event):
+ """ Reimplemented to update the list.
+ """
+ key, text = event.key(), event.text()
+
+ if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter,
+ QtCore.Qt.Key_Tab):
+ self._complete_current()
+ event.accept()
+
+ elif key == QtCore.Qt.Key_Escape:
+ self.hide()
+ event.accept()
+
+ elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down,
+ QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown,
+ QtCore.Qt.Key_Home, QtCore.Qt.Key_End):
+ QtGui.QListWidget.keyPressEvent(self, event)
+ event.accept()
+
+ else:
+ event.ignore()
+
+ def showEvent(self, event):
+ """ Reimplemented to connect the cursor movement handler.
+ """
+ QtGui.QListWidget.showEvent(self, event)
+ self.parent().cursorPositionChanged.connect(self._update_current)
+
+ #--------------------------------------------------------------------------
+ # 'CompletionWidget' interface
+ #--------------------------------------------------------------------------
+
+ def show_items(self, cursor, items):
+ """ Shows the completion widget with 'items' at the position specified
+ by 'cursor'.
+ """
+ text_edit = self.parent()
+ point = text_edit.cursorRect(cursor).bottomRight()
+ point = text_edit.mapToGlobal(point)
+ screen_rect = QtGui.QApplication.desktop().availableGeometry(self)
+ if screen_rect.size().height() - point.y() - self.height() < 0:
+ point = text_edit.mapToGlobal(text_edit.cursorRect().topRight())
+ point.setY(point.y() - self.height())
+ self.move(point)
+
+ self._start_position = cursor.position()
+ self.clear()
+ self.addItems(items)
+ self.setCurrentRow(0)
+ self.show()
+
+ #--------------------------------------------------------------------------
+ # Protected interface
+ #--------------------------------------------------------------------------
+
+ def _complete_current(self):
+ """ Perform the completion with the currently selected item.
+ """
+ self._current_text_cursor().insertText(self.currentItem().text())
+ self.hide()
+
+ def _current_text_cursor(self):
+ """ Returns a cursor with text between the start position and the
+ current position selected.
+ """
+ cursor = self.parent().textCursor()
+ if cursor.position() >= self._start_position:
+ cursor.setPosition(self._start_position,
+ QtGui.QTextCursor.KeepAnchor)
+ return cursor
+
+ def _update_current(self):
+ """ Updates the current item based on the current text.
+ """
+ prefix = self._current_text_cursor().selectedText()
+ if prefix:
+ items = self.findItems(prefix, (QtCore.Qt.MatchStartsWith |
+ QtCore.Qt.MatchCaseSensitive))
+ if items:
+ self.setCurrentItem(items[0])
+ else:
+ self.hide()
+ else:
+ self.hide()
diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py
new file mode 100644
index 0000000..55da0d7
--- /dev/null
+++ b/IPython/frontend/qt/console/console_widget.py
@@ -0,0 +1,650 @@
+# Standard library imports
+import re
+
+# System library imports
+from PyQt4 import QtCore, QtGui
+
+# Local imports
+from completion_widget import CompletionWidget
+
+
+class AnsiCodeProcessor(object):
+ """ Translates ANSI escape codes into readable attributes.
+ """
+
+ def __init__(self):
+ self.ansi_colors = ( # Normal, Bright/Light
+ ('#000000', '#7f7f7f'), # 0: black
+ ('#cd0000', '#ff0000'), # 1: red
+ ('#00cd00', '#00ff00'), # 2: green
+ ('#cdcd00', '#ffff00'), # 3: yellow
+ ('#0000ee', '#0000ff'), # 4: blue
+ ('#cd00cd', '#ff00ff'), # 5: magenta
+ ('#00cdcd', '#00ffff'), # 6: cyan
+ ('#e5e5e5', '#ffffff')) # 7: white
+ self.reset()
+
+ def set_code(self, code):
+ """ Set attributes based on code.
+ """
+ if code == 0:
+ self.reset()
+ elif code == 1:
+ self.intensity = 1
+ self.bold = True
+ elif code == 3:
+ self.italic = True
+ elif code == 4:
+ self.underline = True
+ elif code == 22:
+ self.intensity = 0
+ self.bold = False
+ elif code == 23:
+ self.italic = False
+ elif code == 24:
+ self.underline = False
+ elif code >= 30 and code <= 37:
+ self.foreground_color = code - 30
+ elif code == 39:
+ self.foreground_color = None
+ elif code >= 40 and code <= 47:
+ self.background_color = code - 40
+ elif code == 49:
+ self.background_color = None
+
+ def reset(self):
+ """ Reset attributs to their default values.
+ """
+ self.intensity = 0
+ self.italic = False
+ self.bold = False
+ self.underline = False
+ self.foreground_color = None
+ self.background_color = None
+
+
+class QtAnsiCodeProcessor(AnsiCodeProcessor):
+ """ Translates ANSI escape codes into QTextCharFormats.
+ """
+
+ def get_format(self):
+ """ Returns a QTextCharFormat that encodes the current style attributes.
+ """
+ format = QtGui.QTextCharFormat()
+
+ # Set foreground color
+ if self.foreground_color is not None:
+ color = self.ansi_colors[self.foreground_color][self.intensity]
+ format.setForeground(QtGui.QColor(color))
+
+ # Set background color
+ if self.background_color is not None:
+ color = self.ansi_colors[self.background_color][self.intensity]
+ format.setBackground(QtGui.QColor(color))
+
+ # Set font weight/style options
+ if self.bold:
+ format.setFontWeight(QtGui.QFont.Bold)
+ else:
+ format.setFontWeight(QtGui.QFont.Normal)
+ format.setFontItalic(self.italic)
+ format.setFontUnderline(self.underline)
+
+ return format
+
+
+class ConsoleWidget(QtGui.QPlainTextEdit):
+ """ Base class for console-type widgets. This class is mainly concerned with
+ dealing with the prompt, keeping the cursor inside the editing line, and
+ handling ANSI escape sequences.
+ """
+
+ # Regex to match ANSI escape sequences
+ _ansi_pattern = re.compile('\x01?\x1b\[(.*?)m\x02?')
+
+ # When ctrl is pressed, map certain keys to other keys (without the ctrl):
+ _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
+ QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
+ QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
+ QtCore.Qt.Key_E : QtCore.Qt.Key_End,
+ QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
+ QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
+ QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
+
+ #---------------------------------------------------------------------------
+ # 'QWidget' interface
+ #---------------------------------------------------------------------------
+
+ def __init__(self, parent=None):
+ QtGui.QPlainTextEdit.__init__(self, parent)
+
+ # Initialize public and protected variables
+ self.ansi_codes = True
+ self.continuation_prompt = '> '
+ self.gui_completion = True
+ self._ansi_processor = QtAnsiCodeProcessor()
+ self._completion_widget = CompletionWidget(self)
+ self._executing = False
+ self._prompt = ''
+ self._prompt_pos = 0
+ self._reading = False
+
+ # Configure some basic QPlainTextEdit settings
+ self.setLineWrapMode(QtGui.QPlainTextEdit.WidgetWidth)
+ self.setMaximumBlockCount(500) # Limit text buffer size
+ self.setUndoRedoEnabled(False)
+
+ # Set a monospaced font
+ point_size = QtGui.QApplication.font().pointSize()
+ font = QtGui.QFont('Monospace', point_size)
+ font.setStyleHint(QtGui.QFont.TypeWriter)
+ self._completion_widget.setFont(font)
+ self.document().setDefaultFont(font)
+
+ # Define a custom context menu
+ self._context_menu = QtGui.QMenu(self)
+
+ copy_action = QtGui.QAction('Copy', self)
+ copy_action.triggered.connect(self.copy)
+ self.copyAvailable.connect(copy_action.setEnabled)
+ self._context_menu.addAction(copy_action)
+
+ self._paste_action = QtGui.QAction('Paste', self)
+ self._paste_action.triggered.connect(self.paste)
+ self._context_menu.addAction(self._paste_action)
+ self._context_menu.addSeparator()
+
+ select_all_action = QtGui.QAction('Select All', self)
+ select_all_action.triggered.connect(self.selectAll)
+ self._context_menu.addAction(select_all_action)
+
+ def contextMenuEvent(self, event):
+ """ Reimplemented to create a menu without destructive actions like
+ 'Cut' and 'Delete'.
+ """
+ clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
+ self._paste_action.setEnabled(not clipboard_empty)
+
+ self._context_menu.exec_(event.globalPos())
+
+ def keyPressEvent(self, event):
+ """ Reimplemented to create a console-like interface.
+ """
+ intercepted = False
+ cursor = self.textCursor()
+ position = cursor.position()
+ key = event.key()
+ ctrl_down = event.modifiers() & QtCore.Qt.ControlModifier
+ alt_down = event.modifiers() & QtCore.Qt.AltModifier
+ shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
+
+ if ctrl_down:
+ if key in self._ctrl_down_remap:
+ ctrl_down = False
+ key = self._ctrl_down_remap[key]
+ event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
+ QtCore.Qt.NoModifier)
+
+ elif key == QtCore.Qt.Key_K:
+ if self._in_buffer(position):
+ cursor.movePosition(QtGui.QTextCursor.EndOfLine,
+ QtGui.QTextCursor.KeepAnchor)
+ cursor.removeSelectedText()
+ intercepted = True
+
+ elif key == QtCore.Qt.Key_Y:
+ self.paste()
+ intercepted = True
+
+ elif alt_down:
+ if key == QtCore.Qt.Key_B:
+ self.setTextCursor(self._get_word_start_cursor(position))
+ intercepted = True
+
+ elif key == QtCore.Qt.Key_F:
+ self.setTextCursor(self._get_word_end_cursor(position))
+ intercepted = True
+
+ elif key == QtCore.Qt.Key_Backspace:
+ cursor = self._get_word_start_cursor(position)
+ cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
+ cursor.removeSelectedText()
+ intercepted = True
+
+ elif key == QtCore.Qt.Key_D:
+ cursor = self._get_word_end_cursor(position)
+ cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
+ cursor.removeSelectedText()
+ intercepted = True
+
+ if self._completion_widget.isVisible():
+ self._completion_widget.keyPressEvent(event)
+ intercepted = event.isAccepted()
+
+ else:
+ if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
+ if self._reading:
+ self._reading = False
+ elif not self._executing:
+ self._executing = True
+ self.execute(interactive=True)
+ intercepted = True
+
+ elif key == QtCore.Qt.Key_Up:
+ if self._reading or not self._up_pressed():
+ intercepted = True
+ else:
+ prompt_line = self._get_prompt_cursor().blockNumber()
+ intercepted = cursor.blockNumber() <= prompt_line
+
+ elif key == QtCore.Qt.Key_Down:
+ if self._reading or not self._down_pressed():
+ intercepted = True
+ else:
+ end_line = self._get_end_cursor().blockNumber()
+ intercepted = cursor.blockNumber() == end_line
+
+ elif key == QtCore.Qt.Key_Tab:
+ if self._reading:
+ intercepted = False
+ else:
+ intercepted = not self._tab_pressed()
+
+ elif key == QtCore.Qt.Key_Left:
+ intercepted = not self._in_buffer(position - 1)
+
+ elif key == QtCore.Qt.Key_Home:
+ cursor.movePosition(QtGui.QTextCursor.StartOfLine)
+ start_pos = cursor.position()
+ start_line = cursor.blockNumber()
+ if start_line == self._get_prompt_cursor().blockNumber():
+ start_pos += len(self._prompt)
+ else:
+ start_pos += len(self.continuation_prompt)
+ if shift_down and self._in_buffer(position):
+ self._set_selection(position, start_pos)
+ else:
+ self._set_position(start_pos)
+ intercepted = True
+
+ elif key == QtCore.Qt.Key_Backspace and not alt_down:
+
+ # Line deletion (remove continuation prompt)
+ len_prompt = len(self.continuation_prompt)
+ if cursor.columnNumber() == len_prompt and \
+ position != self._prompt_pos:
+ cursor.setPosition(position - len_prompt,
+ QtGui.QTextCursor.KeepAnchor)
+ cursor.removeSelectedText()
+
+ # Regular backwards deletion
+ else:
+ anchor = cursor.anchor()
+ if anchor == position:
+ intercepted = not self._in_buffer(position - 1)
+ else:
+ intercepted = not self._in_buffer(min(anchor, position))
+
+ elif key == QtCore.Qt.Key_Delete:
+ anchor = cursor.anchor()
+ intercepted = not self._in_buffer(min(anchor, position))
+
+ # Don't move cursor if control is down to allow copy-paste using
+ # the keyboard in any part of the buffer
+ if not ctrl_down:
+ self._keep_cursor_in_buffer()
+
+ if not intercepted:
+ QtGui.QPlainTextEdit.keyPressEvent(self, event)
+
+ #--------------------------------------------------------------------------
+ # 'QPlainTextEdit' interface
+ #--------------------------------------------------------------------------
+
+ def appendPlainText(self, text):
+ """ Reimplemented to not append text as a new paragraph, which doesn't
+ make sense for a console widget. Also, if enabled, handle ANSI
+ codes.
+ """
+ cursor = self.textCursor()
+ cursor.movePosition(QtGui.QTextCursor.End)
+
+ if self.ansi_codes:
+ format = QtGui.QTextCharFormat()
+ previous_end = 0
+ for match in self._ansi_pattern.finditer(text):
+ cursor.insertText(text[previous_end:match.start()], format)
+ previous_end = match.end()
+ for code in match.group(1).split(';'):
+ self._ansi_processor.set_code(int(code))
+ format = self._ansi_processor.get_format()
+ cursor.insertText(text[previous_end:], format)
+ else:
+ cursor.insertText(text)
+
+ def paste(self):
+ """ Reimplemented to ensure that text is pasted in the editing region.
+ """
+ self._keep_cursor_in_buffer()
+ QtGui.QPlainTextEdit.paste(self)
+
+ #---------------------------------------------------------------------------
+ # 'ConsoleWidget' public interface
+ #---------------------------------------------------------------------------
+
+ def execute(self, interactive=False):
+ """ Execute the text in the input buffer. Returns whether the input
+ buffer was completely processed and a new prompt created.
+ """
+ self.appendPlainText('\n')
+ self._prompt_finished()
+ return self._execute(interactive=interactive)
+
+ def _get_input_buffer(self):
+ cursor = self._get_end_cursor()
+ cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
+
+ # Use QTextDocumentFragment intermediate object because it strips
+ # out the Unicode line break characters that Qt insists on inserting.
+ input_buffer = str(cursor.selection().toPlainText())
+
+ # Strip out continuation prompts
+ return input_buffer.replace('\n' + self.continuation_prompt, '\n')
+
+ def _set_input_buffer(self, string):
+ # Add continuation prompts where necessary
+ lines = string.splitlines()
+ for i in xrange(1, len(lines)):
+ lines[i] = self.continuation_prompt + lines[i]
+ string = '\n'.join(lines)
+
+ # Replace buffer with new text
+ cursor = self._get_end_cursor()
+ cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
+ cursor.insertText(string)
+ self.moveCursor(QtGui.QTextCursor.End)
+
+ input_buffer = property(_get_input_buffer, _set_input_buffer)
+
+ def _get_input_buffer_cursor_line(self):
+ cursor = self.textCursor()
+ if cursor.position() >= self._prompt_pos:
+ text = str(cursor.block().text())
+ if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
+ return text[len(self._prompt):]
+ else:
+ return text[len(self.continuation_prompt):]
+ else:
+ return None
+
+ input_buffer_cursor_line = property(_get_input_buffer_cursor_line)
+
+ #---------------------------------------------------------------------------
+ # 'ConsoleWidget' abstract interface
+ #---------------------------------------------------------------------------
+
+ def _execute(self, interactive):
+ """ Called to execute the input buffer. When triggered by an the enter
+ key press, 'interactive' is True; otherwise, it is False. Returns
+ whether the input buffer was completely processed and a new prompt
+ created.
+ """
+ raise NotImplementedError
+
+ def _prompt_started_hook(self):
+ """ Called immediately after a new prompt is displayed.
+ """
+ pass
+
+ def _prompt_finished_hook(self):
+ """ Called immediately after a prompt is finished, i.e. when some input
+ will be processed and a new prompt displayed.
+ """
+ pass
+
+ def _up_pressed(self):
+ """ Called when the up key is pressed. Returns whether to continue
+ processing the event.
+ """
+ return True
+
+ def _down_pressed(self):
+ """ Called when the down key is pressed. Returns whether to continue
+ processing the event.
+ """
+ return True
+
+ def _tab_pressed(self):
+ """ Called when the tab key is pressed. Returns whether to continue
+ processing the event.
+ """
+ return False
+
+ #--------------------------------------------------------------------------
+ # 'ConsoleWidget' protected interface
+ #--------------------------------------------------------------------------
+
+ def _complete_with_items(self, cursor, items):
+ """ Performs completion with 'items' at the specified cursor location.
+ """
+ if len(items) == 1:
+ cursor.setPosition(self.textCursor().position(),
+ QtGui.QTextCursor.KeepAnchor)
+ cursor.insertText(items[0])
+ elif len(items) > 1:
+ if self.gui_completion:
+ self._completion_widget.show_items(cursor, items)
+ else:
+ text = '\n'.join(items) + '\n'
+ self._write_text_keeping_prompt(text)
+
+ def _get_end_cursor(self):
+ """ Convenience method that returns a cursor for the last character.
+ """
+ cursor = self.textCursor()
+ cursor.movePosition(QtGui.QTextCursor.End)
+ return cursor
+
+ def _get_prompt_cursor(self):
+ """ Convenience method that returns a cursor for the prompt position.
+ """
+ cursor = self.textCursor()
+ cursor.setPosition(self._prompt_pos)
+ return cursor
+
+ def _get_selection_cursor(self, start, end):
+ """ Convenience method that returns a cursor with text selected between
+ the positions 'start' and 'end'.
+ """
+ cursor = self.textCursor()
+ cursor.setPosition(start)
+ cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
+ return cursor
+
+ def _get_word_start_cursor(self, position):
+ """ Find the start of the word to the left the given position. If a
+ sequence of non-word characters precedes the first word, skip over
+ them. (This emulates the behavior of bash, emacs, etc.)
+ """
+ document = self.document()
+ position -= 1
+ while self._in_buffer(position) and \
+ not document.characterAt(position).isLetterOrNumber():
+ position -= 1
+ while self._in_buffer(position) and \
+ document.characterAt(position).isLetterOrNumber():
+ position -= 1
+ cursor = self.textCursor()
+ cursor.setPosition(position + 1)
+ return cursor
+
+ def _get_word_end_cursor(self, position):
+ """ Find the end of the word to the right the given position. If a
+ sequence of non-word characters precedes the first word, skip over
+ them. (This emulates the behavior of bash, emacs, etc.)
+ """
+ document = self.document()
+ end = self._get_end_cursor().position()
+ while position < end and \
+ not document.characterAt(position).isLetterOrNumber():
+ position += 1
+ while position < end and \
+ document.characterAt(position).isLetterOrNumber():
+ position += 1
+ cursor = self.textCursor()
+ cursor.setPosition(position)
+ return cursor
+
+ def _prompt_started(self):
+ """ Called immediately after a new prompt is displayed.
+ """
+ self.moveCursor(QtGui.QTextCursor.End)
+ self.centerCursor()
+ self.setReadOnly(False)
+ self._executing = False
+ self._prompt_started_hook()
+
+ def _prompt_finished(self):
+ """ Called immediately after a prompt is finished, i.e. when some input
+ will be processed and a new prompt displayed.
+ """
+ self.setReadOnly(True)
+ self._prompt_finished_hook()
+
+ def _set_position(self, position):
+ """ Convenience method to set the position of the cursor.
+ """
+ cursor = self.textCursor()
+ cursor.setPosition(position)
+ self.setTextCursor(cursor)
+
+ def _set_selection(self, start, end):
+ """ Convenience method to set the current selected text.
+ """
+ self.setTextCursor(self._get_selection_cursor(start, end))
+
+ def _show_prompt(self, prompt):
+ """ Writes a new prompt at the end of the buffer.
+ """
+ self.appendPlainText('\n' + prompt)
+ self._prompt = prompt
+ self._prompt_pos = self._get_end_cursor().position()
+ self._prompt_started()
+
+ def _show_continuation_prompt(self):
+ """ Writes a new continuation prompt at the end of the buffer.
+ """
+ self.appendPlainText(self.continuation_prompt)
+ self._prompt_started()
+
+ def _write_text_keeping_prompt(self, text):
+ """ Writes 'text' after the current prompt, then restores the old prompt
+ with its old input buffer.
+ """
+ input_buffer = self.input_buffer
+ self.appendPlainText('\n')
+ self._prompt_finished()
+
+ self.appendPlainText(text)
+ self._show_prompt(self._prompt)
+ self.input_buffer = input_buffer
+
+ def _in_buffer(self, position):
+ """ Returns whether the given position is inside the editing region.
+ """
+ return position >= self._prompt_pos
+
+ def _keep_cursor_in_buffer(self):
+ """ Ensures that the cursor is inside the editing region. Returns
+ whether the cursor was moved.
+ """
+ cursor = self.textCursor()
+ if cursor.position() < self._prompt_pos:
+ cursor.movePosition(QtGui.QTextCursor.End)
+ self.setTextCursor(cursor)
+ return True
+ else:
+ return False
+
+
+class HistoryConsoleWidget(ConsoleWidget):
+ """ A ConsoleWidget that keeps a history of the commands that have been
+ executed.
+ """
+
+ #---------------------------------------------------------------------------
+ # 'QWidget' interface
+ #---------------------------------------------------------------------------
+
+ def __init__(self, parent=None):
+ super(HistoryConsoleWidget, self).__init__(parent)
+
+ self._history = []
+ self._history_index = 0
+
+ #---------------------------------------------------------------------------
+ # 'ConsoleWidget' public interface
+ #---------------------------------------------------------------------------
+
+ def execute(self, interactive=False):
+ """ Reimplemented to the store history.
+ """
+ stripped = self.input_buffer.rstrip()
+ executed = super(HistoryConsoleWidget, self).execute(interactive)
+ if executed:
+ self._history.append(stripped)
+ self._history_index = len(self._history)
+ return executed
+
+ #---------------------------------------------------------------------------
+ # 'ConsoleWidget' abstract interface
+ #---------------------------------------------------------------------------
+
+ def _up_pressed(self):
+ """ Called when the up key is pressed. Returns whether to continue
+ processing the event.
+ """
+ prompt_cursor = self._get_prompt_cursor()
+ if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
+ self.history_previous()
+
+ # Go to the first line of prompt for seemless history scrolling.
+ cursor = self._get_prompt_cursor()
+ cursor.movePosition(QtGui.QTextCursor.EndOfLine)
+ self.setTextCursor(cursor)
+
+ return False
+ return True
+
+ def _down_pressed(self):
+ """ Called when the down key is pressed. Returns whether to continue
+ processing the event.
+ """
+ end_cursor = self._get_end_cursor()
+ if self.textCursor().blockNumber() == end_cursor.blockNumber():
+ self.history_next()
+ return False
+ return True
+
+ #---------------------------------------------------------------------------
+ # 'HistoryConsoleWidget' interface
+ #---------------------------------------------------------------------------
+
+ def history_previous(self):
+ """ If possible, set the input buffer to the previous item in the
+ history.
+ """
+ if self._history_index > 0:
+ self._history_index -= 1
+ self.input_buffer = self._history[self._history_index]
+
+ def history_next(self):
+ """ Set the input buffer to the next item in the history, or a blank
+ line if there is no subsequent item.
+ """
+ if self._history_index < len(self._history):
+ self._history_index += 1
+ if self._history_index < len(self._history):
+ self.input_buffer = self._history[self._history_index]
+ else:
+ self.input_buffer = ''
diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py
new file mode 100644
index 0000000..d52ec9b
--- /dev/null
+++ b/IPython/frontend/qt/console/frontend_widget.py
@@ -0,0 +1,383 @@
+# Standard library imports
+from codeop import CommandCompiler
+from threading import Thread
+import time
+import types
+
+# System library imports
+from IPython.zmq.session import Message, Session
+from pygments.lexers import PythonLexer
+from PyQt4 import QtCore, QtGui
+import zmq
+
+# ETS imports
+from enthought.pyface.ui.qt4.code_editor.pygments_highlighter import \
+ PygmentsHighlighter
+
+# Local imports
+from call_tip_widget import CallTipWidget
+from completion_lexer import CompletionLexer
+from console_widget import HistoryConsoleWidget
+
+
+class FrontendReplyThread(Thread, QtCore.QObject):
+ """ A Thread that receives a reply from the kernel for the frontend.
+ """
+
+ finished = QtCore.pyqtSignal()
+ output_received = QtCore.pyqtSignal(Message)
+ reply_received = QtCore.pyqtSignal(Message)
+
+ def __init__(self, parent):
+ """ Create a FrontendReplyThread for the specified frontend.
+ """
+ assert isinstance(parent, FrontendWidget)
+ QtCore.QObject.__init__(self, parent)
+ Thread.__init__(self)
+
+ self.sleep_time = 0.05
+
+ def run(self):
+ """ The starting point for the thread.
+ """
+ frontend = self.parent()
+ while True:
+ rep = frontend._recv_reply()
+ if rep is not None:
+ self._recv_output()
+ self.reply_received.emit(rep)
+ break
+
+ self._recv_output()
+ time.sleep(self.sleep_time)
+
+ self.finished.emit()
+
+ def _recv_output(self):
+ """ Send any output to the frontend.
+ """
+ frontend = self.parent()
+ omsgs = frontend._recv_output()
+ for omsg in omsgs:
+ self.output_received.emit(omsg)
+
+
+class FrontendHighlighter(PygmentsHighlighter):
+ """ A Python PygmentsHighlighter that can be turned on and off and which
+ knows about continuation prompts.
+ """
+
+ def __init__(self, frontend):
+ PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
+ self._current_offset = 0
+ self._frontend = frontend
+ self.highlighting_on = False
+
+ def highlightBlock(self, qstring):
+ """ Highlight a block of text. Reimplemented to highlight selectively.
+ """
+ if self.highlighting_on:
+ for prompt in (self._frontend._prompt,
+ self._frontend.continuation_prompt):
+ if qstring.startsWith(prompt):
+ qstring.remove(0, len(prompt))
+ self._current_offset = len(prompt)
+ break
+ PygmentsHighlighter.highlightBlock(self, qstring)
+
+ def setFormat(self, start, count, format):
+ """ Reimplemented to avoid highlighting continuation prompts.
+ """
+ start += self._current_offset
+ PygmentsHighlighter.setFormat(self, start, count, format)
+
+
+class FrontendWidget(HistoryConsoleWidget):
+ """ A Qt frontend for an IPython kernel.
+ """
+
+ # Emitted when an 'execute_reply' is received from the kernel.
+ executed = QtCore.pyqtSignal(Message)
+
+ #---------------------------------------------------------------------------
+ # 'QWidget' interface
+ #---------------------------------------------------------------------------
+
+ def __init__(self, parent=None, session=None, request_socket=None,
+ sub_socket=None):
+ super(FrontendWidget, self).__init__(parent)
+ self.continuation_prompt = '... '
+
+ self._call_tip_widget = CallTipWidget(self)
+ self._compile = CommandCompiler()
+ self._completion_lexer = CompletionLexer(PythonLexer())
+ self._highlighter = FrontendHighlighter(self)
+
+ self.session = Session() if session is None else session
+ self.request_socket = request_socket
+ self.sub_socket = sub_socket
+
+ self.document().contentsChange.connect(self._document_contents_change)
+
+ self._kernel_connected() # XXX
+
+ def focusOutEvent(self, event):
+ """ Reimplemented to hide calltips.
+ """
+ self._call_tip_widget.hide()
+ return super(FrontendWidget, self).focusOutEvent(event)
+
+ def keyPressEvent(self, event):
+ """ Reimplemented to hide calltips.
+ """
+ if event.key() == QtCore.Qt.Key_Escape:
+ self._call_tip_widget.hide()
+ return super(FrontendWidget, self).keyPressEvent(event)
+
+ #---------------------------------------------------------------------------
+ # 'ConsoleWidget' abstract interface
+ #---------------------------------------------------------------------------
+
+ def _execute(self, interactive):
+ """ Called to execute the input buffer. When triggered by an the enter
+ key press, 'interactive' is True; otherwise, it is False. Returns
+ whether the input buffer was completely processed and a new prompt
+ created.
+ """
+ return self.execute_source(self.input_buffer, interactive=interactive)
+
+ def _prompt_started_hook(self):
+ """ Called immediately after a new prompt is displayed.
+ """
+ self._highlighter.highlighting_on = True
+
+ def _prompt_finished_hook(self):
+ """ Called immediately after a prompt is finished, i.e. when some input
+ will be processed and a new prompt displayed.
+ """
+ self._highlighter.highlighting_on = False
+
+ def _tab_pressed(self):
+ """ Called when the tab key is pressed. Returns whether to continue
+ processing the event.
+ """
+ self._keep_cursor_in_buffer()
+ cursor = self.textCursor()
+ if not self._complete():
+ cursor.insertText(' ')
+ return False
+
+ #---------------------------------------------------------------------------
+ # 'FrontendWidget' interface
+ #---------------------------------------------------------------------------
+
+ def execute_source(self, source, hidden=False, interactive=False):
+ """ Execute a string containing Python code. If 'hidden', no output is
+ shown. Returns whether the source executed (i.e., returns True only
+ if no more input is necessary).
+ """
+ try:
+ code = self._compile(source, symbol='single')
+ except (OverflowError, SyntaxError, ValueError):
+ # Just let IPython deal with the syntax error.
+ code = Exception
+
+ # Only execute interactive multiline input if it ends with a blank line
+ lines = source.splitlines()
+ if interactive and len(lines) > 1 and lines[-1].strip() != '':
+ code = None
+
+ executed = code is not None
+ if executed:
+ msg = self.session.send(self.request_socket, 'execute_request',
+ dict(code=source))
+ thread = FrontendReplyThread(self)
+ if not hidden:
+ thread.output_received.connect(self._handle_output)
+ thread.reply_received.connect(self._handle_reply)
+ thread.finished.connect(thread.deleteLater)
+ thread.start()
+ else:
+ space = 0
+ for char in lines[-1]:
+ if char == '\t':
+ space += 4
+ elif char == ' ':
+ space += 1
+ else:
+ break
+ if source.endswith(':') or source.endswith(':\n'):
+ space += 4
+ self._show_continuation_prompt()
+ self.appendPlainText(' ' * space)
+
+ return executed
+
+ def execute_file(self, path, hidden=False):
+ """ Attempts to execute file with 'path'. If 'hidden', no output is
+ shown.
+ """
+ self.execute_source('run %s' % path, hidden=hidden)
+
+ #---------------------------------------------------------------------------
+ # 'FrontendWidget' protected interface
+ #---------------------------------------------------------------------------
+
+ def _call_tip(self):
+ """ Shows a call tip, if appropriate, at the current cursor location.
+ """
+ # Decide if it makes sense to show a call tip
+ cursor = self.textCursor()
+ cursor.movePosition(QtGui.QTextCursor.Left)
+ document = self.document()
+ if document.characterAt(cursor.position()).toAscii() != '(':
+ return False
+ context = self._get_context(cursor)
+ if not context:
+ return False
+
+ # Send the metadata request to the kernel
+ text = '.'.join(context)
+ msg = self.session.send(self.request_socket, 'metadata_request',
+ dict(context=text))
+
+ # Give the kernel some time to respond
+ rep = self._recv_reply_now('metadata_reply')
+ doc = rep.content.docstring if rep else ''
+
+ # Show the call tip
+ if doc:
+ self._call_tip_widget.show_tip(doc)
+ return True
+
+ def _complete(self):
+ """ Performs completion at the current cursor location.
+ """
+ # Decide if it makes sense to do completion
+ context = self._get_context()
+ if not context:
+ return False
+
+ # Send the completion request to the kernel
+ text = '.'.join(context)
+ line = self.input_buffer_cursor_line
+ msg = self.session.send(self.request_socket, 'complete_request',
+ dict(text=text, line=line))
+
+ # Give the kernel some time to respond
+ rep = self._recv_reply_now('complete_reply')
+ matches = rep.content.matches if rep else []
+
+ # Show the completion at the correct location
+ cursor = self.textCursor()
+ cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
+ self._complete_with_items(cursor, matches)
+ return True
+
+ def _kernel_connected(self):
+ """ Called when the frontend is connected to a kernel.
+ """
+ self._show_prompt('>>> ')
+
+ def _get_context(self, cursor=None):
+ """ Gets the context at the current cursor location.
+ """
+ if cursor is None:
+ cursor = self.textCursor()
+ cursor.movePosition(QtGui.QTextCursor.StartOfLine,
+ QtGui.QTextCursor.KeepAnchor)
+ text = unicode(cursor.selectedText())
+ return self._completion_lexer.get_context(text)
+
+ #------ Signal handlers ----------------------------------------------------
+
+ def _document_contents_change(self, position, removed, added):
+ """ Called whenever the document's content changes. Display a calltip
+ if appropriate.
+ """
+ # Calculate where the cursor should be *after* the change:
+ position += added
+
+ document = self.document()
+ if position == self.textCursor().position():
+ self._call_tip()
+
+ def _handle_output(self, omsg):
+ handler = getattr(self, '_handle_%s' % omsg.msg_type, None)
+ if handler is not None:
+ handler(omsg)
+
+ def _handle_pyout(self, omsg):
+ if omsg.parent_header.session == self.session.session:
+ self.appendPlainText(omsg.content.data + '\n')
+
+ def _handle_stream(self, omsg):
+ self.appendPlainText(omsg.content.data)
+
+ def _handle_reply(self, rep):
+ if rep is not None:
+ if rep.msg_type == 'execute_reply':
+ if rep.content.status == 'error':
+ self.appendPlainText(rep.content.traceback[-1])
+ elif rep.content.status == 'aborted':
+ text = "ERROR: ABORTED\n"
+ ab = self.messages[rep.parent_header.msg_id].content
+ if 'code' in ab:
+ text += ab.code
+ else:
+ text += ab
+ self.appendPlainText(text)
+ self._show_prompt('>>> ')
+ self.executed.emit(rep)
+
+ #------ Communication methods ----------------------------------------------
+
+ def _recv_output(self):
+ omsgs = []
+ while True:
+ omsg = self.session.recv(self.sub_socket)
+ if omsg is None:
+ break
+ else:
+ omsgs.append(omsg)
+ return omsgs
+
+ def _recv_reply(self):
+ return self.session.recv(self.request_socket)
+
+ def _recv_reply_now(self, msg_type):
+ for i in xrange(5):
+ rep = self._recv_reply()
+ if rep is not None and rep.msg_type == msg_type:
+ return rep
+ time.sleep(0.1)
+ return None
+
+
+if __name__ == '__main__':
+ import sys
+
+ # Defaults
+ ip = '127.0.0.1'
+ port_base = 5555
+ connection = ('tcp://%s' % ip) + ':%i'
+ req_conn = connection % port_base
+ sub_conn = connection % (port_base+1)
+
+ # Create initial sockets
+ c = zmq.Context()
+ request_socket = c.socket(zmq.XREQ)
+ request_socket.connect(req_conn)
+ sub_socket = c.socket(zmq.SUB)
+ sub_socket.connect(sub_conn)
+ sub_socket.setsockopt(zmq.SUBSCRIBE, '')
+
+ # Launch application
+ app = QtGui.QApplication(sys.argv)
+ widget = FrontendWidget(request_socket=request_socket,
+ sub_socket=sub_socket)
+ widget.setWindowTitle('Python')
+ widget.resize(640, 480)
+ widget.show()
+ sys.exit(app.exec_())
+
diff --git a/IPython/frontend/qt/console/pygments_highlighter.py b/IPython/frontend/qt/console/pygments_highlighter.py
new file mode 100644
index 0000000..c918a04
--- /dev/null
+++ b/IPython/frontend/qt/console/pygments_highlighter.py
@@ -0,0 +1,221 @@
+#------------------------------------------------------------------------------
+# Copyright (c) 2010, Enthought Inc
+# All rights reserved.
+#
+# This software is provided without warranty under the terms of the BSD license.
+
+#
+# Author: Enthought Inc
+# Description:
+#------------------------------------------------------------------------------
+
+from PyQt4 import QtGui
+
+from pygments.lexer import RegexLexer, _TokenType, Text, Error
+from pygments.lexers import CLexer, CppLexer, PythonLexer
+from pygments.styles.default import DefaultStyle
+from pygments.token import Comment
+
+
+def get_tokens_unprocessed(self, text, stack=('root',)):
+ """ Split ``text`` into (tokentype, text) pairs.
+
+ Monkeypatched to store the final stack on the object itself.
+ """
+ pos = 0
+ tokendefs = self._tokens
+ if hasattr(self, '_epd_state_stack'):
+ statestack = list(self._epd_state_stack)
+ else:
+ statestack = list(stack)
+ statetokens = tokendefs[statestack[-1]]
+ while 1:
+ for rexmatch, action, new_state in statetokens:
+ m = rexmatch(text, pos)
+ if m:
+ if type(action) is _TokenType:
+ yield pos, action, m.group()
+ else:
+ for item in action(self, m):
+ yield item
+ pos = m.end()
+ if new_state is not None:
+ # state transition
+ if isinstance(new_state, tuple):
+ for state in new_state:
+ if state == '#pop':
+ statestack.pop()
+ elif state == '#push':
+ statestack.append(statestack[-1])
+ else:
+ statestack.append(state)
+ elif isinstance(new_state, int):
+ # pop
+ del statestack[new_state:]
+ elif new_state == '#push':
+ statestack.append(statestack[-1])
+ else:
+ assert False, "wrong state def: %r" % new_state
+ statetokens = tokendefs[statestack[-1]]
+ break
+ else:
+ try:
+ if text[pos] == '\n':
+ # at EOL, reset state to "root"
+ pos += 1
+ statestack = ['root']
+ statetokens = tokendefs['root']
+ yield pos, Text, u'\n'
+ continue
+ yield pos, Error, text[pos]
+ pos += 1
+ except IndexError:
+ break
+ self._epd_state_stack = list(statestack)
+
+# Monkeypatch!
+RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
+
+
+# Even with the above monkey patch to store state, multiline comments do not
+# work since they are stateless (Pygments uses a single multiline regex for
+# these comments, but Qt lexes by line). So we need to add a state for comments
+# to the C and C++ lexers. This means that nested multiline comments will appear
+# to be valid C/C++, but this is better than the alternative for now.
+
+def replace_pattern(tokens, new_pattern):
+ """ Given a RegexLexer token dictionary 'tokens', replace all patterns that
+ match the token specified in 'new_pattern' with 'new_pattern'.
+ """
+ for state in tokens.values():
+ for index, pattern in enumerate(state):
+ if isinstance(pattern, tuple) and pattern[1] == new_pattern[1]:
+ state[index] = new_pattern
+
+# More monkeypatching!
+comment_start = (r'/\*', Comment.Multiline, 'comment')
+comment_state = [ (r'[^*/]', Comment.Multiline),
+ (r'/\*', Comment.Multiline, '#push'),
+ (r'\*/', Comment.Multiline, '#pop'),
+ (r'[*/]', Comment.Multiline) ]
+replace_pattern(CLexer.tokens, comment_start)
+replace_pattern(CppLexer.tokens, comment_start)
+CLexer.tokens['comment'] = comment_state
+CppLexer.tokens['comment'] = comment_state
+
+
+class BlockUserData(QtGui.QTextBlockUserData):
+ """ Storage for the user data associated with each line.
+ """
+
+ syntax_stack = ('root',)
+
+ def __init__(self, **kwds):
+ for key, value in kwds.iteritems():
+ setattr(self, key, value)
+ QtGui.QTextBlockUserData.__init__(self)
+
+ def __repr__(self):
+ attrs = ['syntax_stack']
+ kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
+ for attr in attrs ])
+ return 'BlockUserData(%s)' % kwds
+
+
+class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
+ """ Syntax highlighter that uses Pygments for parsing. """
+
+ def __init__(self, parent, lexer=None):
+ super(PygmentsHighlighter, self).__init__(parent)
+
+ self._lexer = lexer if lexer else PythonLexer()
+ self._style = DefaultStyle
+ # Caches for formats and brushes.
+ self._brushes = {}
+ self._formats = {}
+
+ def highlightBlock(self, qstring):
+ """ Highlight a block of text.
+ """
+ qstring = unicode(qstring)
+ prev_data = self.previous_block_data()
+
+ if prev_data is not None:
+ self._lexer._epd_state_stack = prev_data.syntax_stack
+ elif hasattr(self._lexer, '_epd_state_stack'):
+ del self._lexer._epd_state_stack
+
+ index = 0
+ # Lex the text using Pygments
+ for token, text in self._lexer.get_tokens(qstring):
+ l = len(text)
+ format = self._get_format(token)
+ if format is not None:
+ self.setFormat(index, l, format)
+ index += l
+
+ if hasattr(self._lexer, '_epd_state_stack'):
+ data = BlockUserData(syntax_stack=self._lexer._epd_state_stack)
+ self.currentBlock().setUserData(data)
+ # Clean up for the next go-round.
+ del self._lexer._epd_state_stack
+
+ def previous_block_data(self):
+ """ Convenience method for returning the previous block's user data.
+ """
+ return self.currentBlock().previous().userData()
+
+ def _get_format(self, token):
+ """ Returns a QTextCharFormat for token or None.
+ """
+ if token in self._formats:
+ return self._formats[token]
+ result = None
+ for key, value in self._style.style_for_token(token) .items():
+ if value:
+ if result is None:
+ result = QtGui.QTextCharFormat()
+ if key == 'color':
+ result.setForeground(self._get_brush(value))
+ elif key == 'bgcolor':
+ result.setBackground(self._get_brush(value))
+ elif key == 'bold':
+ result.setFontWeight(QtGui.QFont.Bold)
+ elif key == 'italic':
+ result.setFontItalic(True)
+ elif key == 'underline':
+ result.setUnderlineStyle(
+ QtGui.QTextCharFormat.SingleUnderline)
+ elif key == 'sans':
+ result.setFontStyleHint(QtGui.QFont.SansSerif)
+ elif key == 'roman':
+ result.setFontStyleHint(QtGui.QFont.Times)
+ elif key == 'mono':
+ result.setFontStyleHint(QtGui.QFont.TypeWriter)
+ elif key == 'border':
+ # Borders are normally used for errors. We can't do a border
+ # so instead we do a wavy underline
+ result.setUnderlineStyle(
+ QtGui.QTextCharFormat.WaveUnderline)
+ result.setUnderlineColor(self._get_color(value))
+ self._formats[token] = result
+ return result
+
+ def _get_brush(self, color):
+ """ Returns a brush for the color.
+ """
+ result = self._brushes.get(color)
+ if result is None:
+ qcolor = self._get_color(color)
+ result = QtGui.QBrush(qcolor)
+ self._brushes[color] = result
+
+ return result
+
+ def _get_color(self, color):
+ qcolor = QtGui.QColor()
+ qcolor.setRgb(int(color[:2],base=16),
+ int(color[2:4], base=16),
+ int(color[4:6], base=16))
+ return qcolor
+