1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtGui module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qsyntaxhighlighter.h"
41 
42 #ifndef QT_NO_SYNTAXHIGHLIGHTER
43 #include <private/qobject_p.h>
44 #include <qtextdocument.h>
45 #include <private/qtextdocument_p.h>
46 #include <qtextlayout.h>
47 #include <qpointer.h>
48 #include <qscopedvaluerollback.h>
49 #include <qtextobject.h>
50 #include <qtextcursor.h>
51 #include <qdebug.h>
52 #include <qtimer.h>
53 
54 #include <algorithm>
55 
56 QT_BEGIN_NAMESPACE
57 
58 class QSyntaxHighlighterPrivate : public QObjectPrivate
59 {
60     Q_DECLARE_PUBLIC(QSyntaxHighlighter)
61 public:
QSyntaxHighlighterPrivate()62     inline QSyntaxHighlighterPrivate()
63         : rehighlightPending(false), inReformatBlocks(false)
64     {}
65 
66     QPointer<QTextDocument> doc;
67 
68     void _q_reformatBlocks(int from, int charsRemoved, int charsAdded);
69     void reformatBlocks(int from, int charsRemoved, int charsAdded);
70     void reformatBlock(const QTextBlock &block);
71 
rehighlight(QTextCursor & cursor,QTextCursor::MoveOperation operation)72     inline void rehighlight(QTextCursor &cursor, QTextCursor::MoveOperation operation)
73     {
74         QScopedValueRollback<bool> bg(inReformatBlocks, true);
75         cursor.beginEditBlock();
76         int from = cursor.position();
77         cursor.movePosition(operation);
78         reformatBlocks(from, 0, cursor.position() - from);
79         cursor.endEditBlock();
80     }
81 
_q_delayedRehighlight()82     inline void _q_delayedRehighlight() {
83         if (!rehighlightPending)
84             return;
85         rehighlightPending = false;
86         q_func()->rehighlight();
87     }
88 
89     void applyFormatChanges();
90     QVector<QTextCharFormat> formatChanges;
91     QTextBlock currentBlock;
92     bool rehighlightPending;
93     bool inReformatBlocks;
94 };
95 
applyFormatChanges()96 void QSyntaxHighlighterPrivate::applyFormatChanges()
97 {
98     bool formatsChanged = false;
99 
100     QTextLayout *layout = currentBlock.layout();
101 
102     QVector<QTextLayout::FormatRange> ranges = layout->formats();
103 
104     const int preeditAreaStart = layout->preeditAreaPosition();
105     const int preeditAreaLength = layout->preeditAreaText().length();
106 
107     if (preeditAreaLength != 0) {
108         auto isOutsidePreeditArea = [=](const QTextLayout::FormatRange &range) {
109             return range.start < preeditAreaStart
110                     || range.start + range.length > preeditAreaStart + preeditAreaLength;
111         };
112         const auto it = std::remove_if(ranges.begin(), ranges.end(),
113                                        isOutsidePreeditArea);
114         if (it != ranges.end()) {
115             ranges.erase(it, ranges.end());
116             formatsChanged = true;
117         }
118     } else if (!ranges.isEmpty()) {
119         ranges.clear();
120         formatsChanged = true;
121     }
122 
123     int i = 0;
124     while (i < formatChanges.count()) {
125         QTextLayout::FormatRange r;
126 
127         while (i < formatChanges.count() && formatChanges.at(i) == r.format)
128             ++i;
129 
130         if (i == formatChanges.count())
131             break;
132 
133         r.start = i;
134         r.format = formatChanges.at(i);
135 
136         while (i < formatChanges.count() && formatChanges.at(i) == r.format)
137             ++i;
138 
139         Q_ASSERT(i <= formatChanges.count());
140         r.length = i - r.start;
141 
142         if (preeditAreaLength != 0) {
143             if (r.start >= preeditAreaStart)
144                 r.start += preeditAreaLength;
145             else if (r.start + r.length >= preeditAreaStart)
146                 r.length += preeditAreaLength;
147         }
148 
149         ranges << r;
150         formatsChanged = true;
151     }
152 
153     if (formatsChanged) {
154         layout->setFormats(ranges);
155         doc->markContentsDirty(currentBlock.position(), currentBlock.length());
156     }
157 }
158 
_q_reformatBlocks(int from,int charsRemoved,int charsAdded)159 void QSyntaxHighlighterPrivate::_q_reformatBlocks(int from, int charsRemoved, int charsAdded)
160 {
161     if (!inReformatBlocks && !rehighlightPending)
162         reformatBlocks(from, charsRemoved, charsAdded);
163 }
164 
reformatBlocks(int from,int charsRemoved,int charsAdded)165 void QSyntaxHighlighterPrivate::reformatBlocks(int from, int charsRemoved, int charsAdded)
166 {
167     QTextBlock block = doc->findBlock(from);
168     if (!block.isValid())
169         return;
170 
171     int endPosition;
172     QTextBlock lastBlock = doc->findBlock(from + charsAdded + (charsRemoved > 0 ? 1 : 0));
173     if (lastBlock.isValid())
174         endPosition = lastBlock.position() + lastBlock.length();
175     else
176         endPosition = doc->docHandle()->length();
177 
178     bool forceHighlightOfNextBlock = false;
179 
180     while (block.isValid() && (block.position() < endPosition || forceHighlightOfNextBlock)) {
181         const int stateBeforeHighlight = block.userState();
182 
183         reformatBlock(block);
184 
185         forceHighlightOfNextBlock = (block.userState() != stateBeforeHighlight);
186 
187         block = block.next();
188     }
189 
190     formatChanges.clear();
191 }
192 
reformatBlock(const QTextBlock & block)193 void QSyntaxHighlighterPrivate::reformatBlock(const QTextBlock &block)
194 {
195     Q_Q(QSyntaxHighlighter);
196 
197     Q_ASSERT_X(!currentBlock.isValid(), "QSyntaxHighlighter::reformatBlock()", "reFormatBlock() called recursively");
198 
199     currentBlock = block;
200 
201     formatChanges.fill(QTextCharFormat(), block.length() - 1);
202     q->highlightBlock(block.text());
203     applyFormatChanges();
204 
205     currentBlock = QTextBlock();
206 }
207 
208 /*!
209     \class QSyntaxHighlighter
210     \reentrant
211     \inmodule QtGui
212 
213     \brief The QSyntaxHighlighter class allows you to define syntax
214     highlighting rules, and in addition you can use the class to query
215     a document's current formatting or user data.
216 
217     \since 4.1
218 
219     \ingroup richtext-processing
220 
221     The QSyntaxHighlighter class is a base class for implementing
222     QTextDocument syntax highlighters.  A syntax highligher automatically
223     highlights parts of the text in a QTextDocument. Syntax highlighters are
224     often used when the user is entering text in a specific format (for example source code)
225     and help the user to read the text and identify syntax errors.
226 
227     To provide your own syntax highlighting, you must subclass
228     QSyntaxHighlighter and reimplement highlightBlock().
229 
230     When you create an instance of your QSyntaxHighlighter subclass,
231     pass it the QTextDocument that you want the syntax
232     highlighting to be applied to. For example:
233 
234     \snippet code/src_gui_text_qsyntaxhighlighter.cpp 0
235 
236     After this your highlightBlock() function will be called
237     automatically whenever necessary. Use your highlightBlock()
238     function to apply formatting (e.g. setting the font and color) to
239     the text that is passed to it. QSyntaxHighlighter provides the
240     setFormat() function which applies a given QTextCharFormat on
241     the current text block. For example:
242 
243     \snippet code/src_gui_text_qsyntaxhighlighter.cpp 1
244 
245     \target QSyntaxHighlighter multiblock
246 
247     Some syntaxes can have constructs that span several text
248     blocks. For example, a C++ syntax highlighter should be able to
249     cope with \c{/}\c{*...*}\c{/} multiline comments. To deal with
250     these cases it is necessary to know the end state of the previous
251     text block (e.g. "in comment").
252 
253     Inside your highlightBlock() implementation you can query the end
254     state of the previous text block using the previousBlockState()
255     function. After parsing the block you can save the last state
256     using setCurrentBlockState().
257 
258     The currentBlockState() and previousBlockState() functions return
259     an int value. If no state is set, the returned value is -1. You
260     can designate any other value to identify any given state using
261     the setCurrentBlockState() function. Once the state is set the
262     QTextBlock keeps that value until it is set set again or until the
263     corresponding paragraph of text is deleted.
264 
265     For example, if you're writing a simple C++ syntax highlighter,
266     you might designate 1 to signify "in comment":
267 
268     \snippet code/src_gui_text_qsyntaxhighlighter.cpp 2
269 
270     In the example above, we first set the current block state to
271     0. Then, if the previous block ended within a comment, we highlight
272     from the beginning of the current block (\c {startIndex =
273     0}). Otherwise, we search for the given start expression. If the
274     specified end expression cannot be found in the text block, we
275     change the current block state by calling setCurrentBlockState(),
276     and make sure that the rest of the block is highlighted.
277 
278     In addition you can query the current formatting and user data
279     using the format() and currentBlockUserData() functions
280     respectively. You can also attach user data to the current text
281     block using the setCurrentBlockUserData() function.
282     QTextBlockUserData can be used to store custom settings. In the
283     case of syntax highlighting, it is in particular interesting as
284     cache storage for information that you may figure out while
285     parsing the paragraph's text. For an example, see the
286     setCurrentBlockUserData() documentation.
287 
288     \sa QTextDocument, {Syntax Highlighter Example}
289 */
290 
291 /*!
292     Constructs a QSyntaxHighlighter with the given \a parent.
293 
294     If the parent is a QTextEdit, it installs the syntax highlighter on the
295     parents document. The specified QTextEdit also becomes the owner of
296     the QSyntaxHighlighter.
297 */
QSyntaxHighlighter(QObject * parent)298 QSyntaxHighlighter::QSyntaxHighlighter(QObject *parent)
299     : QObject(*new QSyntaxHighlighterPrivate, parent)
300 {
301     if (parent && parent->inherits("QTextEdit")) {
302         QTextDocument *doc = qvariant_cast<QTextDocument *>(parent->property("document"));
303         if (doc)
304             setDocument(doc);
305     }
306 }
307 
308 /*!
309     Constructs a QSyntaxHighlighter and installs it on \a parent.
310     The specified QTextDocument also becomes the owner of the
311     QSyntaxHighlighter.
312 */
QSyntaxHighlighter(QTextDocument * parent)313 QSyntaxHighlighter::QSyntaxHighlighter(QTextDocument *parent)
314     : QObject(*new QSyntaxHighlighterPrivate, parent)
315 {
316     setDocument(parent);
317 }
318 
319 /*!
320     Destructor. Uninstalls this syntax highlighter from the text document.
321 */
~QSyntaxHighlighter()322 QSyntaxHighlighter::~QSyntaxHighlighter()
323 {
324     setDocument(nullptr);
325 }
326 
327 /*!
328     Installs the syntax highlighter on the given QTextDocument \a doc.
329     A QSyntaxHighlighter can only be used with one document at a time.
330 */
setDocument(QTextDocument * doc)331 void QSyntaxHighlighter::setDocument(QTextDocument *doc)
332 {
333     Q_D(QSyntaxHighlighter);
334     if (d->doc) {
335         disconnect(d->doc, SIGNAL(contentsChange(int,int,int)),
336                    this, SLOT(_q_reformatBlocks(int,int,int)));
337 
338         QTextCursor cursor(d->doc);
339         cursor.beginEditBlock();
340         for (QTextBlock blk = d->doc->begin(); blk.isValid(); blk = blk.next())
341             blk.layout()->clearFormats();
342         cursor.endEditBlock();
343     }
344     d->doc = doc;
345     if (d->doc) {
346         connect(d->doc, SIGNAL(contentsChange(int,int,int)),
347                 this, SLOT(_q_reformatBlocks(int,int,int)));
348         if (!d->doc->isEmpty()) {
349             d->rehighlightPending = true;
350             QTimer::singleShot(0, this, SLOT(_q_delayedRehighlight()));
351         }
352     }
353 }
354 
355 /*!
356     Returns the QTextDocument on which this syntax highlighter is
357     installed.
358 */
document() const359 QTextDocument *QSyntaxHighlighter::document() const
360 {
361     Q_D(const QSyntaxHighlighter);
362     return d->doc;
363 }
364 
365 /*!
366     \since 4.2
367 
368     Reapplies the highlighting to the whole document.
369 
370     \sa rehighlightBlock()
371 */
rehighlight()372 void QSyntaxHighlighter::rehighlight()
373 {
374     Q_D(QSyntaxHighlighter);
375     if (!d->doc)
376         return;
377 
378     QTextCursor cursor(d->doc);
379     d->rehighlight(cursor, QTextCursor::End);
380     d->rehighlightPending = false; // user manually did a full rehighlight
381 }
382 
383 /*!
384     \since 4.6
385 
386     Reapplies the highlighting to the given QTextBlock \a block.
387 
388     \sa rehighlight()
389 */
rehighlightBlock(const QTextBlock & block)390 void QSyntaxHighlighter::rehighlightBlock(const QTextBlock &block)
391 {
392     Q_D(QSyntaxHighlighter);
393     if (!d->doc || !block.isValid() || block.document() != d->doc)
394         return;
395 
396     const bool rehighlightPending = d->rehighlightPending;
397 
398     QTextCursor cursor(block);
399     d->rehighlight(cursor, QTextCursor::EndOfBlock);
400 
401     if (rehighlightPending)
402         d->rehighlightPending = rehighlightPending;
403 }
404 
405 /*!
406     \fn void QSyntaxHighlighter::highlightBlock(const QString &text)
407 
408     Highlights the given text block. This function is called when
409     necessary by the rich text engine, i.e. on text blocks which have
410     changed.
411 
412     To provide your own syntax highlighting, you must subclass
413     QSyntaxHighlighter and reimplement highlightBlock(). In your
414     reimplementation you should parse the block's \a text and call
415     setFormat() as often as necessary to apply any font and color
416     changes that you require. For example:
417 
418     \snippet code/src_gui_text_qsyntaxhighlighter.cpp 1
419 
420     See the \l{QSyntaxHighlighter multiblock}{Detailed Description} for
421     examples of using setCurrentBlockState(), currentBlockState()
422     and previousBlockState() to handle syntaxes with constructs that
423     span several text blocks
424 
425     \sa previousBlockState(), setFormat(), setCurrentBlockState()
426 */
427 
428 /*!
429     This function is applied to the syntax highlighter's current text
430     block (i.e. the text that is passed to the highlightBlock()
431     function).
432 
433     The specified \a format is applied to the text from the \a start
434     position for a length of \a count characters (if \a count is 0,
435     nothing is done). The formatting properties set in \a format are
436     merged at display time with the formatting information stored
437     directly in the document, for example as previously set with
438     QTextCursor's functions. Note that the document itself remains
439     unmodified by the format set through this function.
440 
441     \sa format(), highlightBlock()
442 */
setFormat(int start,int count,const QTextCharFormat & format)443 void QSyntaxHighlighter::setFormat(int start, int count, const QTextCharFormat &format)
444 {
445     Q_D(QSyntaxHighlighter);
446     if (start < 0 || start >= d->formatChanges.count())
447         return;
448 
449     const int end = qMin(start + count, d->formatChanges.count());
450     for (int i = start; i < end; ++i)
451         d->formatChanges[i] = format;
452 }
453 
454 /*!
455     \overload
456 
457     The specified \a color is applied to the current text block from
458     the \a start position for a length of \a count characters.
459 
460     The other attributes of the current text block, e.g. the font and
461     background color, are reset to default values.
462 
463     \sa format(), highlightBlock()
464 */
setFormat(int start,int count,const QColor & color)465 void QSyntaxHighlighter::setFormat(int start, int count, const QColor &color)
466 {
467     QTextCharFormat format;
468     format.setForeground(color);
469     setFormat(start, count, format);
470 }
471 
472 /*!
473     \overload
474 
475     The specified \a font is applied to the current text block from
476     the \a start position for a length of \a count characters.
477 
478     The other attributes of the current text block, e.g. the font and
479     background color, are reset to default values.
480 
481     \sa format(), highlightBlock()
482 */
setFormat(int start,int count,const QFont & font)483 void QSyntaxHighlighter::setFormat(int start, int count, const QFont &font)
484 {
485     QTextCharFormat format;
486     format.setFont(font);
487     setFormat(start, count, format);
488 }
489 
490 /*!
491     \fn QTextCharFormat QSyntaxHighlighter::format(int position) const
492 
493     Returns the format at \a position inside the syntax highlighter's
494     current text block.
495 */
format(int pos) const496 QTextCharFormat QSyntaxHighlighter::format(int pos) const
497 {
498     Q_D(const QSyntaxHighlighter);
499     if (pos < 0 || pos >= d->formatChanges.count())
500         return QTextCharFormat();
501     return d->formatChanges.at(pos);
502 }
503 
504 /*!
505     Returns the end state of the text block previous to the
506     syntax highlighter's current block. If no value was
507     previously set, the returned value is -1.
508 
509     \sa highlightBlock(), setCurrentBlockState()
510 */
previousBlockState() const511 int QSyntaxHighlighter::previousBlockState() const
512 {
513     Q_D(const QSyntaxHighlighter);
514     if (!d->currentBlock.isValid())
515         return -1;
516 
517     const QTextBlock previous = d->currentBlock.previous();
518     if (!previous.isValid())
519         return -1;
520 
521     return previous.userState();
522 }
523 
524 /*!
525     Returns the state of the current text block. If no value is set,
526     the returned value is -1.
527 */
currentBlockState() const528 int QSyntaxHighlighter::currentBlockState() const
529 {
530     Q_D(const QSyntaxHighlighter);
531     if (!d->currentBlock.isValid())
532         return -1;
533 
534     return d->currentBlock.userState();
535 }
536 
537 /*!
538     Sets the state of the current text block to \a newState.
539 
540     \sa highlightBlock()
541 */
setCurrentBlockState(int newState)542 void QSyntaxHighlighter::setCurrentBlockState(int newState)
543 {
544     Q_D(QSyntaxHighlighter);
545     if (!d->currentBlock.isValid())
546         return;
547 
548     d->currentBlock.setUserState(newState);
549 }
550 
551 /*!
552     Attaches the given \a data to the current text block.  The
553     ownership is passed to the underlying text document, i.e. the
554     provided QTextBlockUserData object will be deleted if the
555     corresponding text block gets deleted.
556 
557     QTextBlockUserData can be used to store custom settings. In the
558     case of syntax highlighting, it is in particular interesting as
559     cache storage for information that you may figure out while
560     parsing the paragraph's text.
561 
562     For example while parsing the text, you can keep track of
563     parenthesis characters that you encounter ('{[(' and the like),
564     and store their relative position and the actual QChar in a simple
565     class derived from QTextBlockUserData:
566 
567     \snippet code/src_gui_text_qsyntaxhighlighter.cpp 3
568 
569     During cursor navigation in the associated editor, you can ask the
570     current QTextBlock (retrieved using the QTextCursor::block()
571     function) if it has a user data object set and cast it to your \c
572     BlockData object. Then you can check if the current cursor
573     position matches with a previously recorded parenthesis position,
574     and, depending on the type of parenthesis (opening or closing),
575     find the next opening or closing parenthesis on the same level.
576 
577     In this way you can do a visual parenthesis matching and highlight
578     from the current cursor position to the matching parenthesis. That
579     makes it easier to spot a missing parenthesis in your code and to
580     find where a corresponding opening/closing parenthesis is when
581     editing parenthesis intensive code.
582 
583     \sa QTextBlock::setUserData()
584 */
setCurrentBlockUserData(QTextBlockUserData * data)585 void QSyntaxHighlighter::setCurrentBlockUserData(QTextBlockUserData *data)
586 {
587     Q_D(QSyntaxHighlighter);
588     if (!d->currentBlock.isValid())
589         return;
590 
591     d->currentBlock.setUserData(data);
592 }
593 
594 /*!
595     Returns the QTextBlockUserData object previously attached to the
596     current text block.
597 
598     \sa QTextBlock::userData(), setCurrentBlockUserData()
599 */
currentBlockUserData() const600 QTextBlockUserData *QSyntaxHighlighter::currentBlockUserData() const
601 {
602     Q_D(const QSyntaxHighlighter);
603     if (!d->currentBlock.isValid())
604         return nullptr;
605 
606     return d->currentBlock.userData();
607 }
608 
609 /*!
610     \since 4.4
611 
612     Returns the current text block.
613 */
currentBlock() const614 QTextBlock QSyntaxHighlighter::currentBlock() const
615 {
616     Q_D(const QSyntaxHighlighter);
617     return d->currentBlock;
618 }
619 
620 QT_END_NAMESPACE
621 
622 #include "moc_qsyntaxhighlighter.cpp"
623 
624 #endif // QT_NO_SYNTAXHIGHLIGHTER
625