File: highlighter.py

package info (click to toggle)
frescobaldi 3.0.0~git20161001.0.eec60717%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 19,792 kB
  • ctags: 5,843
  • sloc: python: 37,853; sh: 180; makefile: 69
file content (265 lines) | stat: -rw-r--r-- 9,724 bytes parent folder | download | duplicates (2)
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()