diff --git a/IPython/frontend/qt/console/bracket_matcher.py b/IPython/frontend/qt/console/bracket_matcher.py new file mode 100644 index 0000000..4c0b6a7 --- /dev/null +++ b/IPython/frontend/qt/console/bracket_matcher.py @@ -0,0 +1,101 @@ +""" Provides bracket matching for Q[Plain]TextEdit widgets. +""" + +# System library imports +from PyQt4 import QtCore, QtGui + + +class BracketMatcher(QtCore.QObject): + """ Matches square brackets, braces, and parentheses based on cursor + position. + """ + + # Protected class variables. + _opening_map = { '(':')', '{':'}', '[':']' } + _closing_map = { ')':'(', '}':'{', ']':'[' } + + #-------------------------------------------------------------------------- + # 'QObject' interface + #-------------------------------------------------------------------------- + + def __init__(self, parent, multiline=True): + """ Create a call tip manager that is attached to the specified Qt + text edit widget. + """ + assert isinstance(parent, (QtGui.QTextEdit, QtGui.QPlainTextEdit)) + QtCore.QObject.__init__(self, parent) + + # The format to apply to matching brackets. + self.format = QtGui.QTextCharFormat() + self.format.setBackground(QtGui.QColor('silver')) + + parent.cursorPositionChanged.connect(self._cursor_position_changed) + + #-------------------------------------------------------------------------- + # Protected interface + #-------------------------------------------------------------------------- + + def _find_match(self, position): + """ Given a valid position in the text document, try to find the + position of the matching bracket. Returns -1 if unsuccessful. + """ + # Decide what character to search for and what direction to search in. + document = self.parent().document() + qchar = document.characterAt(position) + start_char = qchar.toAscii() + search_char = self._opening_map.get(start_char) + if search_char: + increment = 1 + else: + search_char = self._closing_map.get(start_char) + if search_char: + increment = -1 + else: + return -1 + + # Search for the character. + depth = 0 + while position >= 0 and position < document.characterCount(): + char = qchar.toAscii() + if char == start_char: + depth += 1 + elif char == search_char: + depth -= 1 + if depth == 0: + break + position += increment + qchar = document.characterAt(position) + else: + position = -1 + return position + + def _selection_for_character(self, position): + """ Convenience method for selecting a character. + """ + selection = QtGui.QTextEdit.ExtraSelection() + cursor = self.parent().textCursor() + cursor.setPosition(position) + cursor.movePosition(QtGui.QTextCursor.NextCharacter, + QtGui.QTextCursor.KeepAnchor) + selection.cursor = cursor + selection.format = self.format + return selection + + #------ Signal handlers ---------------------------------------------------- + + def _cursor_position_changed(self): + """ Updates the document formatting based on the new cursor position. + """ + # Clear out the old formatting. + text_edit = self.parent() + text_edit.setExtraSelections([]) + + # Attempt to match a bracket for the new cursor position. + cursor = text_edit.textCursor() + if not cursor.hasSelection(): + position = cursor.position() - 1 + match_position = self._find_match(position) + if match_position != -1: + extra_selections = [ self._selection_for_character(pos) + for pos in (position, match_position) ] + text_edit.setExtraSelections(extra_selections) diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py index 658f861..01ac331 100644 --- a/IPython/frontend/qt/console/frontend_widget.py +++ b/IPython/frontend/qt/console/frontend_widget.py @@ -9,7 +9,8 @@ from PyQt4 import QtCore, QtGui # Local imports from IPython.core.inputsplitter import InputSplitter from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin -from IPython.utils.traitlets import Bool, Type +from IPython.utils.traitlets import Bool +from bracket_matcher import BracketMatcher from call_tip_widget import CallTipWidget from completion_lexer import CompletionLexer from console_widget import HistoryConsoleWidget @@ -88,8 +89,7 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): executed = QtCore.pyqtSignal(object) # Protected class variables. - _highlighter_class = Type(FrontendHighlighter) - _input_splitter_class = Type(InputSplitter) + _input_splitter_class = InputSplitter #--------------------------------------------------------------------------- # 'object' interface @@ -99,10 +99,11 @@ class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin): super(FrontendWidget, self).__init__(*args, **kw) # FrontendWidget protected variables. + self._bracket_matcher = BracketMatcher(self._control) self._call_tip_widget = CallTipWidget(self._control) self._completion_lexer = CompletionLexer(PythonLexer()) self._hidden = False - self._highlighter = self._highlighter_class(self) + self._highlighter = FrontendHighlighter(self) self._input_splitter = self._input_splitter_class(input_mode='block') self._kernel_manager = None diff --git a/IPython/frontend/qt/console/pygments_highlighter.py b/IPython/frontend/qt/console/pygments_highlighter.py index adc4abc..c32079d 100644 --- a/IPython/frontend/qt/console/pygments_highlighter.py +++ b/IPython/frontend/qt/console/pygments_highlighter.py @@ -110,14 +110,12 @@ class PygmentsHighlighter(QtGui.QSyntaxHighlighter): elif hasattr(self._lexer, '_saved_state_stack'): del self._lexer._saved_state_stack - index = 0 # Lex the text using Pygments + index = 0 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 + length = len(text) + self.setFormat(index, length, self._get_format(token)) + index += length if hasattr(self._lexer, '_saved_state_stack'): data = PygmentsBlockUserData( @@ -185,11 +183,9 @@ class PygmentsHighlighter(QtGui.QSyntaxHighlighter): def _get_format_from_style(self, token, style): """ Returns a QTextCharFormat for token by reading a Pygments style. """ - result = None + result = QtGui.QTextCharFormat() for key, value in 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':