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 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | class QSyntaxHighlighterPrivate : public QObjectPrivate |
23 | { |
24 | Q_DECLARE_PUBLIC(QSyntaxHighlighter) |
25 | public: |
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 | |
60 | void 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 | |
119 | void QSyntaxHighlighterPrivate::_q_reformatBlocks(int from, int charsRemoved, int charsAdded) |
120 | { |
121 | if (!inReformatBlocks && !rehighlightPending) |
122 | reformatBlocks(from, charsRemoved, charsAdded); |
123 | } |
124 | |
125 | void 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 | |
153 | void 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 | */ |
258 | QSyntaxHighlighter::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 | */ |
273 | QSyntaxHighlighter::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 | */ |
282 | QSyntaxHighlighter::~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 | */ |
291 | void 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 | */ |
319 | QTextDocument *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 | */ |
332 | void 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 | */ |
350 | void 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 | */ |
403 | void 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 | */ |
425 | void 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 | */ |
443 | void 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 | */ |
456 | QTextCharFormat 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 | */ |
471 | int 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 | */ |
488 | int 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 | */ |
502 | void 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 | */ |
545 | void 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 | */ |
560 | QTextBlockUserData *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 | */ |
574 | QTextBlock QSyntaxHighlighter::currentBlock() const |
575 | { |
576 | Q_D(const QSyntaxHighlighter); |
577 | return d->currentBlock; |
578 | } |
579 | |
580 | QT_END_NAMESPACE |
581 | |
582 | #include "moc_qsyntaxhighlighter.cpp" |
583 | |
584 | #endif // QT_NO_SYNTAXHIGHLIGHTER |
585 | |