264 lines
9.6 KiB
Python
264 lines
9.6 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
This module contains the symbol matcher mode
|
|
"""
|
|
from pyqode.core.api import get_block_symbol_data
|
|
from pyqode.core.api.decoration import TextDecoration
|
|
from pyqode.core.api.mode import Mode
|
|
from pyqode.qt import QtGui
|
|
|
|
|
|
#: symbols indices in SymbolMatcherMode.SYMBOLS map
|
|
PAREN = 0
|
|
SQUARE = 1
|
|
BRACE = 2
|
|
|
|
#: character indices in SymbolMatcherMode.SYMBOLS map
|
|
OPEN = 0
|
|
CLOSE = 1
|
|
|
|
|
|
class SymbolMatcherMode(Mode):
|
|
""" Highlights matching symbols (parentheses, braces,...)
|
|
|
|
.. note:: This mode requires the document to be filled with
|
|
:class:`pyqode.core.api.TextBlockUserData`, i.e. a
|
|
:class:`pyqode.core.api.SyntaxHighlighter` must be installed on
|
|
the editor instance.
|
|
|
|
"""
|
|
#: known symbols {SYMBOL: (OPEN, CLOSE)}, you can customise this map to
|
|
#: add support for other symbols
|
|
SYMBOLS = {
|
|
PAREN: ('(', ')'),
|
|
SQUARE: ('[', ']'),
|
|
BRACE: ('{', '}')
|
|
}
|
|
|
|
@property
|
|
def match_background(self):
|
|
"""
|
|
Background color of matching symbols.
|
|
"""
|
|
return self._match_background
|
|
|
|
@match_background.setter
|
|
def match_background(self, value):
|
|
self._match_background = value
|
|
self._refresh_decorations()
|
|
if self.editor:
|
|
for clone in self.editor.clones:
|
|
try:
|
|
clone.modes.get(self.__class__).match_background = value
|
|
except KeyError:
|
|
# this should never happen since we're working with clones
|
|
pass
|
|
|
|
@property
|
|
def match_foreground(self):
|
|
"""
|
|
Foreground color of matching symbols.
|
|
"""
|
|
return self._match_foreground
|
|
|
|
@match_foreground.setter
|
|
def match_foreground(self, value):
|
|
self._match_foreground = value
|
|
self._refresh_decorations()
|
|
if self.editor:
|
|
for clone in self.editor.clones:
|
|
try:
|
|
clone.modes.get(self.__class__).match_foreground = value
|
|
except KeyError:
|
|
# this should never happen since we're working with clones
|
|
pass
|
|
|
|
@property
|
|
def unmatch_background(self):
|
|
"""
|
|
Background color of non-matching symbols.
|
|
"""
|
|
return self._unmatch_background
|
|
|
|
@unmatch_background.setter
|
|
def unmatch_background(self, value):
|
|
self._unmatch_background = value
|
|
self._refresh_decorations()
|
|
if self.editor:
|
|
for clone in self.editor.clones:
|
|
try:
|
|
clone.modes.get(self.__class__).unmatch_background = value
|
|
except KeyError:
|
|
# this should never happen since we're working with clones
|
|
pass
|
|
|
|
@property
|
|
def unmatch_foreground(self):
|
|
"""
|
|
Foreground color of matching symbols.
|
|
"""
|
|
return self._unmatch_foreground
|
|
|
|
@unmatch_foreground.setter
|
|
def unmatch_foreground(self, value):
|
|
self._unmatch_foreground = value
|
|
self._refresh_decorations()
|
|
if self.editor:
|
|
for clone in self.editor.clones:
|
|
try:
|
|
clone.modes.get(self.__class__).unmatch_foreground = value
|
|
except KeyError:
|
|
# this should never happen since we're working with clones
|
|
pass
|
|
|
|
def __init__(self):
|
|
super(SymbolMatcherMode, self).__init__()
|
|
self._decorations = []
|
|
self._match_background = QtGui.QBrush(QtGui.QColor('#B4EEB4'))
|
|
self._match_foreground = QtGui.QColor('red')
|
|
self._unmatch_background = QtGui.QBrush(QtGui.QColor('transparent'))
|
|
self._unmatch_foreground = QtGui.QColor('red')
|
|
|
|
def _clear_decorations(self):
|
|
for deco in self._decorations:
|
|
self.editor.decorations.remove(deco)
|
|
self._decorations[:] = []
|
|
|
|
def symbol_pos(self, cursor, character_type=OPEN, symbol_type=PAREN):
|
|
"""
|
|
Find the corresponding symbol position (line, column) of the specified
|
|
symbol. If symbol type is PAREN and character_type is OPEN, the
|
|
function will look for '('.
|
|
|
|
:param cursor: QTextCursor
|
|
:param character_type: character type to look for (open or close char)
|
|
:param symbol_type: symbol type (index in the SYMBOLS map).
|
|
"""
|
|
retval = None, None
|
|
original_cursor = self.editor.textCursor()
|
|
self.editor.setTextCursor(cursor)
|
|
block = cursor.block()
|
|
data = get_block_symbol_data(self.editor, block)
|
|
self._match(symbol_type, data, block.position())
|
|
for deco in self._decorations:
|
|
if deco.character == self.SYMBOLS[symbol_type][character_type]:
|
|
retval = deco.line, deco.column
|
|
break
|
|
self.editor.setTextCursor(original_cursor)
|
|
self._clear_decorations()
|
|
return retval
|
|
|
|
def _refresh_decorations(self):
|
|
for deco in self._decorations:
|
|
self.editor.decorations.remove(deco)
|
|
if deco.match:
|
|
deco.set_foreground(self._match_foreground)
|
|
deco.set_background(self._match_background)
|
|
else:
|
|
deco.set_foreground(self._unmatch_foreground)
|
|
deco.set_background(self._unmatch_background)
|
|
self.editor.decorations.append(deco)
|
|
|
|
def on_state_changed(self, state):
|
|
if state:
|
|
self.editor.cursorPositionChanged.connect(self.do_symbols_matching)
|
|
else:
|
|
self.editor.cursorPositionChanged.disconnect(
|
|
self.do_symbols_matching)
|
|
|
|
def _match(self, symbol, data, cursor_pos):
|
|
symbols = data[symbol]
|
|
for i, info in enumerate(symbols):
|
|
pos = (self.editor.textCursor().position() -
|
|
self.editor.textCursor().block().position())
|
|
if info.character == self.SYMBOLS[symbol][OPEN] and \
|
|
info.position == pos:
|
|
self._create_decoration(
|
|
cursor_pos + info.position,
|
|
self._match_left(
|
|
symbol, self.editor.textCursor().block(), i + 1, 0))
|
|
elif info.character == self.SYMBOLS[symbol][CLOSE] and \
|
|
info.position == pos - 1:
|
|
self._create_decoration(
|
|
cursor_pos + info.position,
|
|
self._match_right(
|
|
symbol, self.editor.textCursor().block(), i - 1, 0))
|
|
|
|
def _match_left(self, symbol, current_block, i, cpt):
|
|
while current_block.isValid():
|
|
data = get_block_symbol_data(self.editor, current_block)
|
|
parentheses = data[symbol]
|
|
for j in range(i, len(parentheses)):
|
|
info = parentheses[j]
|
|
if info.character == self.SYMBOLS[symbol][OPEN]:
|
|
cpt += 1
|
|
continue
|
|
if info.character == self.SYMBOLS[symbol][CLOSE] and cpt == 0:
|
|
self._create_decoration(current_block.position() +
|
|
info.position)
|
|
return True
|
|
elif info.character == self.SYMBOLS[symbol][CLOSE]:
|
|
cpt -= 1
|
|
current_block = current_block.next()
|
|
i = 0
|
|
return False
|
|
|
|
def _match_right(self, symbol, current_block, i, nb_right_paren):
|
|
while current_block.isValid():
|
|
data = get_block_symbol_data(self.editor, current_block)
|
|
parentheses = data[symbol]
|
|
for j in range(i, -1, -1):
|
|
if j >= 0:
|
|
info = parentheses[j]
|
|
if info.character == self.SYMBOLS[symbol][CLOSE]:
|
|
nb_right_paren += 1
|
|
continue
|
|
if info.character == self.SYMBOLS[symbol][OPEN]:
|
|
if nb_right_paren == 0:
|
|
self._create_decoration(
|
|
current_block.position() + info.position)
|
|
return True
|
|
else:
|
|
nb_right_paren -= 1
|
|
current_block = current_block.previous()
|
|
data = get_block_symbol_data(self.editor, current_block)
|
|
parentheses = data[symbol]
|
|
i = len(parentheses) - 1
|
|
return False
|
|
|
|
def do_symbols_matching(self):
|
|
"""
|
|
Performs symbols matching.
|
|
"""
|
|
self._clear_decorations()
|
|
current_block = self.editor.textCursor().block()
|
|
data = get_block_symbol_data(self.editor, current_block)
|
|
pos = self.editor.textCursor().block().position()
|
|
for symbol in [PAREN, SQUARE, BRACE]:
|
|
self._match(symbol, data, pos)
|
|
|
|
def _create_decoration(self, pos, match=True):
|
|
cursor = self.editor.textCursor()
|
|
cursor.setPosition(pos)
|
|
cursor.movePosition(cursor.NextCharacter, cursor.KeepAnchor)
|
|
deco = TextDecoration(cursor, draw_order=10)
|
|
deco.line = cursor.blockNumber()
|
|
deco.column = cursor.columnNumber()
|
|
deco.character = cursor.selectedText()
|
|
deco.match = match
|
|
if match:
|
|
deco.set_foreground(self._match_foreground)
|
|
deco.set_background(self._match_background)
|
|
else:
|
|
deco.set_foreground(self._unmatch_foreground)
|
|
deco.set_background(self._unmatch_background)
|
|
self._decorations.append(deco)
|
|
self.editor.decorations.append(deco)
|
|
return cursor
|
|
|
|
def clone_settings(self, original):
|
|
self.match_background = original.match_background
|
|
self.match_foreground = original.match_foreground
|
|
self.unmatch_background = original.unmatch_background
|
|
self.unmatch_foreground = original.unmatch_foreground
|