1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
|
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
#
# Copyright (c) 2008 - 2014 by Wilbert Berendsen
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# See http://www.gnu.org/licenses/ for more information.
"""
The Highlighter class provides syntax highlighting and more information
about a document's contents.
"""
from PyQt5.QtGui import (
QColor, QSyntaxHighlighter, QTextBlockUserData, QTextCharFormat,
QTextCursor, QTextDocument)
import ly.lex
import ly.colorize
import app
import cursortools
import textformats
import metainfo
import plugin
import variables
import documentinfo
metainfo.define('highlighting', True)
def mapping(data):
"""Return a dictionary mapping token classes from ly.lex to QTextCharFormats.
The QTextFormats are queried from the specified TextFormatData instance.
The returned dictionary is a ly.colorize.Mapping instance.
"""
return ly.colorize.Mapper((cls, data.textFormat(mode, style.name))
for mode, styles in ly.colorize.default_mapping()
for style in styles
for cls in style.classes)
def highlighter(document):
"""Return the Highlighter for this document."""
return Highlighter.instance(document)
def highlight_mapping():
"""Return the global Mapping instance that maps token class to QTextCharFormat."""
global _highlight_mapping
try:
return _highlight_mapping
except NameError:
_highlight_mapping = mapping(textformats.formatData('editor'))
return _highlight_mapping
def _reset_highlight_mapping():
"""Remove the global HighlightFormats instance, so it's recreated next time."""
global _highlight_mapping
try:
del _highlight_mapping
except NameError:
pass
app.settingsChanged.connect(_reset_highlight_mapping, -100) # before all others
class Highlighter(plugin.Plugin, QSyntaxHighlighter):
"""A QSyntaxHighlighter that can highlight a QTextDocument.
It can be used for both generic QTextDocuments as well as
document.Document objects. In the latter case it automatically:
- initializes whether highlighting is enabled from the document's metainfo
- picks the mode from the variables if they specify that
The Highlighter automatically re-reads the highlighting settings if they
are changed.
"""
def __init__(self, document):
QSyntaxHighlighter.__init__(self, document)
self._fridge = ly.lex.Fridge()
app.settingsChanged.connect(self.rehighlight)
self._initialState = None
self._highlighting = True
self._mode = None
self.initializeDocument()
def initializeDocument(self):
"""This method is always called by the __init__ method.
The default implementation does nothing for generic QTextDocuments,
but for document.Document instances it connects to some additional
signals to keep the mode up-to-date (reading it from the variables if
needed) and initializes whether to enable visual highlighting from the
document's metainfo.
"""
document = self.document()
if hasattr(document, 'url'):
self._highlighting = metainfo.info(document).highlighting
document.loaded.connect(self._resetHighlighting)
self._mode = documentinfo.mode(document, False)
variables.manager(document).changed.connect(self._variablesChange)
def _variablesChange(self):
"""Called whenever the variables have changed. Checks the mode."""
mode = documentinfo.mode(self.document(), False)
if mode != self._mode:
self._mode = mode
self.rehighlight()
def _resetHighlighting(self):
"""Switch highlighting on or off depending on saved metainfo."""
self.setHighlighting(metainfo.info(self.document()).highlighting)
def highlightBlock(self, text):
"""Called by Qt when the highlighting of the current line needs updating."""
# find the state of the previous line
prev = self.previousBlockState()
state = self._fridge.thaw(prev)
blank = not state and (not text or text.isspace())
if not state:
state = self.initialState()
# collect and save the tokens
tokens = tuple(state.tokens(text))
cursortools.data(self.currentBlock()).tokens = tokens
# if blank thus far, keep the highlighter coming back
# because the parsing state is not yet known; else save the state
self.setCurrentBlockState(prev - 1 if blank else self._fridge.freeze(state))
# apply highlighting if desired
if self._highlighting:
setFormat = lambda f: self.setFormat(token.pos, len(token), f)
mapping = highlight_mapping()
for token in tokens:
f = mapping[token]
if f:
setFormat(f)
def setHighlighting(self, enable):
"""Enable or disable highlighting."""
changed = enable != self._highlighting
self._highlighting = enable
if changed:
self.rehighlight()
def isHighlighting(self):
"""Return whether highlighting is active."""
return self._highlighting
def state(self, block):
"""Return a thawed ly.lex.State() object at the *end* of the QTextBlock.
Do not use this method directly. Instead use tokeniter.state() or
tokeniter.state_end(), because that assures the highlighter has run
at least once.
"""
return self._fridge.thaw(block.userState()) or self.initialState()
def setInitialState(self, state):
"""Force the initial state. Use None to enable auto-detection."""
self._initialState = self._fridge.freeze(state) if state else None
def initialState(self):
"""Return the initial State for this document."""
if self._initialState is None:
mode = self._mode or ly.lex.guessMode(self.document().toPlainText())
return ly.lex.state(mode)
return self._fridge.thaw(self._initialState)
def html_copy(cursor, scheme='editor', number_lines=False):
"""Return a new QTextDocument with highlighting set as HTML textcharformats.
The cursor is a cursor of a document.Document instance. If the cursor
has a selection, only the selection is put in the new document.
If number_lines is True, line numbers are added.
"""
data = textformats.formatData(scheme)
doc = QTextDocument()
doc.setDefaultFont(data.font)
doc.setPlainText(cursor.document().toPlainText())
if metainfo.info(cursor.document()).highlighting:
highlight(doc, mapping(data), ly.lex.state(documentinfo.mode(cursor.document())))
if cursor.hasSelection():
# cut out not selected text
start, end = cursor.selectionStart(), cursor.selectionEnd()
cur1 = QTextCursor(doc)
cur1.setPosition(start, QTextCursor.KeepAnchor)
cur2 = QTextCursor(doc)
cur2.setPosition(end)
cur2.movePosition(QTextCursor.End, QTextCursor.KeepAnchor)
cur2.removeSelectedText()
cur1.removeSelectedText()
if number_lines:
c = QTextCursor(doc)
f = QTextCharFormat()
f.setBackground(QColor('#eeeeee'))
if cursor.hasSelection():
num = cursor.document().findBlock(cursor.selectionStart()).blockNumber() + 1
last = cursor.document().findBlock(cursor.selectionEnd())
else:
num = 1
last = cursor.document().lastBlock()
lastnum = last.blockNumber() + 1
padding = len(format(lastnum))
block = doc.firstBlock()
while block.isValid():
c.setPosition(block.position())
c.setCharFormat(f)
c.insertText('{0:>{1}d} '.format(num, padding))
block = block.next()
num += 1
return doc
def highlight(document, mapping=None, state=None):
"""Highlight a generic QTextDocument once.
mapping is an optional Mapping instance, defaulting to the current
configured editor highlighting formats (returned by highlight_mapping()).
state is an optional ly.lex.State instance. By default the text type is
guessed.
"""
if mapping is None:
mapping = highlight_mapping()
if state is None:
state = ly.lex.guessState(document.toPlainText())
cursor = QTextCursor(document)
block = document.firstBlock()
while block.isValid():
for token in state.tokens(block.text()):
f = mapping[token]
if f:
cursor.setPosition(block.position() + token.pos)
cursor.setPosition(block.position() + token.end, QTextCursor.KeepAnchor)
cursor.setCharFormat(f)
block = block.next()
|