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