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

source code of qtbase/src/gui/text/qsyntaxhighlighter.cpp