Cppcheck
codeeditor.cpp
Go to the documentation of this file.
1 /*
2  * Cppcheck - A tool for static C/C++ code analysis
3  * Copyright (C) 2007-2024 Cppcheck team.
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (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, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "codeeditor.h"
20 
21 #include "codeeditorstyle.h"
22 
23 #include <QChar>
24 #include <QColor>
25 #include <QCryptographicHash>
26 #include <QFont>
27 #include <QFontMetrics>
28 #include <QKeySequence>
29 #include <QLatin1Char>
30 #include <QList>
31 #include <QPainter>
32 #include <QPaintEvent>
33 #include <QRect>
34 #include <QRectF>
35 #include <QRegularExpressionMatchIterator>
36 #include <QShortcut>
37 #include <QTextBlock>
38 #include <QTextCursor>
39 #include <QTextEdit>
40 #include <QTextFormat>
41 
42 class QTextDocument;
43 
44 
45 Highlighter::Highlighter(QTextDocument *parent,
46  CodeEditorStyle *widgetStyle) :
47  QSyntaxHighlighter(parent),
48  mWidgetStyle(widgetStyle)
49 {
50  HighlightingRule rule;
51 
54  QStringList keywordPatterns;
55  // TODO: use Keywords::getX()
56  keywordPatterns << "alignas"
57  << "alignof"
58  << "asm"
59  << "auto"
60  << "bool"
61  << "break"
62  << "case"
63  << "catch"
64  << "char"
65  << "char8_t"
66  << "char16_t"
67  << "char32_t"
68  << "class"
69  << "concept"
70  << "const"
71  << "consteval"
72  << "constexpr"
73  << "constinit"
74  << "const_cast"
75  << "continue"
76  << "co_await"
77  << "co_return"
78  << "co_yield"
79  << "decltype"
80  << "default"
81  << "delete"
82  << "do"
83  << "double"
84  << "dynamic_cast"
85  << "else"
86  << "enum"
87  << "explicit"
88  << "export"
89  << "extern"
90  << "false"
91  << "final"
92  << "float"
93  << "for"
94  << "friend"
95  << "goto"
96  << "if"
97  << "import"
98  << "inline"
99  << "int"
100  << "long"
101  << "module"
102  << "mutable"
103  << "namespace"
104  << "new"
105  << "noexcept"
106  << "nullptr"
107  << "operator"
108  << "override"
109  << "private"
110  << "protected"
111  << "public"
112  << "reinterpret_cast"
113  << "requires"
114  << "return"
115  << "short"
116  << "signed"
117  << "static"
118  << "static_assert"
119  << "static_cast"
120  << "struct"
121  << "switch"
122  << "template"
123  << "this"
124  << "thread_local"
125  << "throw"
126  << "true"
127  << "try"
128  << "typedef"
129  << "typeid"
130  << "typename"
131  << "union"
132  << "unsigned"
133  << "virtual"
134  << "void"
135  << "volatile"
136  << "wchar_t"
137  << "while";
138  for (const QString &pattern : keywordPatterns) {
139  rule.pattern = QRegularExpression("\\b" + pattern + "\\b");
140  rule.format = mKeywordFormat;
141  rule.ruleRole = RuleRole::Keyword;
142  mHighlightingRules.append(rule);
143  }
144 
145  mClassFormat.setForeground(mWidgetStyle->classColor);
146  mClassFormat.setFontWeight(mWidgetStyle->classWeight);
147  rule.pattern = QRegularExpression("\\bQ[A-Za-z]+\\b");
148  rule.format = mClassFormat;
149  rule.ruleRole = RuleRole::Class;
150  mHighlightingRules.append(rule);
151 
152  mQuotationFormat.setForeground(mWidgetStyle->quoteColor);
153  mQuotationFormat.setFontWeight(mWidgetStyle->quoteWeight);
154  // We use lazy `*?` instead greed `*` quantifier to find the real end of the c-string.
155  // We use negative lookbehind assertion `(?<!\‍)` to ignore `\"` sequence in the c-string.
156  rule.pattern = QRegularExpression("\".*?(?<!\\\\)\"");
157  rule.format = mQuotationFormat;
158  rule.ruleRole = RuleRole::Quote;
159  mHighlightingRules.append(rule);
160 
163  rule.pattern = QRegularExpression("//[^\n]*");
165  rule.ruleRole = RuleRole::Comment;
166  mHighlightingRules.append(rule);
167 
169 
172 
173  mSymbolFormat.setForeground(mWidgetStyle->symbolFGColor);
174  mSymbolFormat.setBackground(mWidgetStyle->symbolBGColor);
175  mSymbolFormat.setFontWeight(mWidgetStyle->symbolWeight);
176 
177  // We use negative lookbehind assertion `(?<!/)`
178  // to ignore case: single line comment and line of asterisk
179  mCommentStartExpression = QRegularExpression("(?<!/)/\\*");
180  mCommentEndExpression = QRegularExpression("\\*/");
181 }
182 
183 void Highlighter::setSymbols(const QStringList &symbols)
184 {
186  for (const QString &sym : symbols) {
187  HighlightingRule rule;
188  rule.pattern = QRegularExpression("\\b" + sym + "\\b");
189  rule.format = mSymbolFormat;
190  rule.ruleRole = RuleRole::Symbol;
191  mHighlightingRulesWithSymbols.append(rule);
192  }
193 }
194 
196 {
197  mKeywordFormat.setForeground(newStyle.keywordColor);
198  mKeywordFormat.setFontWeight(newStyle.keywordWeight);
199  mClassFormat.setForeground(newStyle.classColor);
200  mClassFormat.setFontWeight(newStyle.classWeight);
201  mSingleLineCommentFormat.setForeground(newStyle.commentColor);
202  mSingleLineCommentFormat.setFontWeight(newStyle.commentWeight);
203  mMultiLineCommentFormat.setForeground(newStyle.commentColor);
204  mMultiLineCommentFormat.setFontWeight(newStyle.commentWeight);
205  mQuotationFormat.setForeground(newStyle.quoteColor);
206  mQuotationFormat.setFontWeight(newStyle.quoteWeight);
207  mSymbolFormat.setForeground(newStyle.symbolFGColor);
208  mSymbolFormat.setBackground(newStyle.symbolBGColor);
209  mSymbolFormat.setFontWeight(newStyle.symbolWeight);
210  for (HighlightingRule& rule : mHighlightingRules) {
211  applyFormat(rule);
212  }
213 
215  applyFormat(rule);
216  }
217 }
218 
219 void Highlighter::highlightBlock(const QString &text)
220 {
221  for (const HighlightingRule &rule : mHighlightingRulesWithSymbols) {
222  QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text);
223  while (matchIterator.hasNext()) {
224  QRegularExpressionMatch match = matchIterator.next();
225  setFormat(match.capturedStart(), match.capturedLength(), rule.format);
226  }
227  }
228 
229  setCurrentBlockState(0);
230 
231  int startIndex = 0;
232  if (previousBlockState() != 1)
233  startIndex = text.indexOf(mCommentStartExpression);
234 
235  while (startIndex >= 0) {
236  QRegularExpressionMatch match = mCommentEndExpression.match(text, startIndex);
237  const int endIndex = match.capturedStart();
238  int commentLength = 0;
239  if (endIndex == -1) {
240  setCurrentBlockState(1);
241  commentLength = text.length() - startIndex;
242  } else {
243  commentLength = endIndex - startIndex
244  + match.capturedLength();
245  }
246  setFormat(startIndex, commentLength, mMultiLineCommentFormat);
247  startIndex = text.indexOf(mCommentStartExpression, startIndex + commentLength);
248  }
249 }
250 
252 {
253  switch (rule.ruleRole) {
254  case RuleRole::Keyword:
255  rule.format = mKeywordFormat;
256  break;
257  case RuleRole::Class:
258  rule.format = mClassFormat;
259  break;
260  case RuleRole::Comment:
262  break;
263  case RuleRole::Quote:
264  rule.format = mQuotationFormat;
265  break;
266  case RuleRole::Symbol:
267  rule.format = mSymbolFormat;
268  break;
269  }
270 }
271 
272 CodeEditor::CodeEditor(QWidget *parent) :
273  QPlainTextEdit(parent),
274  mWidgetStyle(new CodeEditorStyle(defaultStyleLight))
275 {
276  mLineNumberArea = new LineNumberArea(this);
277  mHighlighter = new Highlighter(document(), mWidgetStyle);
278  mErrorPosition = -1;
279 
280  QFont font("Monospace");
281  font.setStyleHint(QFont::TypeWriter);
282  setFont(font);
283  mLineNumberArea->setFont(font);
284 
285  // set widget coloring by overriding widget style sheet
286  setObjectName("CodeEditor");
287  setStyleSheet(generateStyleString());
288 
289 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
290  auto *copyText = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_C),this);
291  auto *allText = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_A),this);
292 #else
293  const auto *copyText = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_C),this);
294  const auto *allText = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_A),this);
295 #endif
296 
297  connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateLineNumberAreaWidth(int)));
298  connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateLineNumberArea(QRect,int)));
299  connect(copyText, SIGNAL(activated()), this, SLOT(copy()));
300  connect(allText, SIGNAL(activated()), this, SLOT(selectAll()));
301 
303 }
304 
306 {
307  // NOTE: not a Qt Object - delete manually
308  delete mWidgetStyle;
309 }
310 
311 static int getPos(const QString &fileData, int lineNumber)
312 {
313  if (lineNumber <= 1)
314  return 0;
315  for (int pos = 0, line = 1; pos < fileData.size(); ++pos) {
316  if (fileData[pos] != '\n')
317  continue;
318  ++line;
319  if (line >= lineNumber)
320  return pos + 1;
321  }
322  return fileData.size();
323 }
324 
326 {
327  *mWidgetStyle = newStyle;
328  // apply new styling
329  setStyleSheet(generateStyleString());
330  mHighlighter->setStyle(newStyle);
331  mHighlighter->rehighlight();
333 }
334 
335 void CodeEditor::setError(const QString &code, int errorLine, const QStringList &symbols)
336 {
337  mHighlighter->setSymbols(symbols);
338 
339  setPlainText(code);
340 
341  mErrorPosition = getPos(code, errorLine);
342  QTextCursor tc = textCursor();
343  tc.setPosition(mErrorPosition);
344  setTextCursor(tc);
345  centerCursor();
346 
348 }
349 
350 void CodeEditor::setError(int errorLine, const QStringList &symbols)
351 {
352  mHighlighter->setSymbols(symbols);
353 
354  mErrorPosition = getPos(toPlainText(), errorLine);
355  QTextCursor tc = textCursor();
356  tc.setPosition(mErrorPosition);
357  setTextCursor(tc);
358  centerCursor();
359 
361 }
362 
364 {
365  int digits = 1;
366  int max = qMax(1, blockCount());
367  while (max >= 10) {
368  max /= 10;
369  ++digits;
370  }
371 
372 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
373  const int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
374 #else
375  const int space = 3 + fontMetrics().width(QLatin1Char('9')) * digits;
376 #endif
377  return space;
378 }
379 
380 void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
381 {
382  setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
383 }
384 
385 void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
386 {
387  if (dy)
388  mLineNumberArea->scroll(0, dy);
389  else
390  mLineNumberArea->update(0, rect.y(), mLineNumberArea->width(), rect.height());
391 
392  if (rect.contains(viewport()->rect()))
394 }
395 
396 void CodeEditor::resizeEvent(QResizeEvent *event)
397 {
398  QPlainTextEdit::resizeEvent(event);
399  QRect cr = contentsRect();
400  mLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
401 }
402 
404 {
405  QList<QTextEdit::ExtraSelection> extraSelections;
406 
407  QTextEdit::ExtraSelection selection;
408 
409  selection.format.setBackground(mWidgetStyle->highlightBGColor);
410  selection.format.setProperty(QTextFormat::FullWidthSelection, true);
411  selection.cursor = QTextCursor(document());
412  if (mErrorPosition >= 0) {
413  selection.cursor.setPosition(mErrorPosition);
414  } else {
415  selection.cursor.setPosition(0);
416  }
417  selection.cursor.clearSelection();
418  extraSelections.append(selection);
419 
420  setExtraSelections(extraSelections);
421 }
422 
423 void CodeEditor::lineNumberAreaPaintEvent(const QPaintEvent *event)
424 {
425  QPainter painter(mLineNumberArea);
426  painter.fillRect(event->rect(), mWidgetStyle->lineNumBGColor);
427 
428  QTextBlock block = firstVisibleBlock();
429  int blockNumber = block.blockNumber();
430  int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
431  int bottom = top + (int) blockBoundingRect(block).height();
432 
433  while (block.isValid() && top <= event->rect().bottom()) {
434  if (block.isVisible() && bottom >= event->rect().top()) {
435  QString number = QString::number(blockNumber + 1);
436  painter.setPen(mWidgetStyle->lineNumFGColor);
437  painter.drawText(0, top, mLineNumberArea->width(), fontMetrics().height(),
438  Qt::AlignRight, number);
439  }
440 
441  block = block.next();
442  top = bottom;
443  bottom = top + (int) blockBoundingRect(block).height();
444  ++blockNumber;
445  }
446 }
447 
449 {
450  QString bgcolor = QString("background:rgb(%1,%2,%3);")
451  .arg(mWidgetStyle->widgetBGColor.red())
452  .arg(mWidgetStyle->widgetBGColor.green())
453  .arg(mWidgetStyle->widgetBGColor.blue());
454  QString fgcolor = QString("color:rgb(%1,%2,%3);")
455  .arg(mWidgetStyle->widgetFGColor.red())
456  .arg(mWidgetStyle->widgetFGColor.green())
457  .arg(mWidgetStyle->widgetFGColor.blue());
458  QString style = QString("%1 %2")
459  .arg(bgcolor)
460  .arg(fgcolor);
461  return style;
462 }
static bool match(const Token *tok, const std::string &rhs)
Definition: astutils.cpp:342
QFont::Weight classWeight
QFont::Weight commentWeight
QFont::Weight quoteWeight
QFont::Weight symbolWeight
QFont::Weight keywordWeight
~CodeEditor() override
Definition: codeeditor.cpp:305
void updateLineNumberAreaWidth(int newBlockCount)
Definition: codeeditor.cpp:380
Highlighter * mHighlighter
Definition: codeeditor.h:139
void lineNumberAreaPaintEvent(const QPaintEvent *event)
Definition: codeeditor.cpp:423
void updateLineNumberArea(const QRect &, int)
Definition: codeeditor.cpp:385
QWidget * mLineNumberArea
Definition: codeeditor.h:138
int mErrorPosition
Definition: codeeditor.h:141
void setStyle(const CodeEditorStyle &newStyle)
Definition: codeeditor.cpp:325
void resizeEvent(QResizeEvent *event) override
Definition: codeeditor.cpp:396
void highlightErrorLine()
Definition: codeeditor.cpp:403
CodeEditorStyle * mWidgetStyle
Definition: codeeditor.h:140
QString generateStyleString()
Definition: codeeditor.cpp:448
void setError(const QString &code, int errorLine, const QStringList &symbols)
Set source code to show, goto error line and highlight that line.
Definition: codeeditor.cpp:335
CodeEditor(QWidget *parent)
Definition: codeeditor.cpp:272
int lineNumberAreaWidth()
Definition: codeeditor.cpp:363
QRegularExpression mCommentStartExpression
Definition: codeeditor.h:72
QTextCharFormat mKeywordFormat
Definition: codeeditor.h:75
QTextCharFormat mSingleLineCommentFormat
Definition: codeeditor.h:77
QTextCharFormat mClassFormat
Definition: codeeditor.h:76
void applyFormat(HighlightingRule &rule)
Definition: codeeditor.cpp:251
Highlighter(QTextDocument *parent, CodeEditorStyle *widgetStyle)
Definition: codeeditor.cpp:45
QVector< HighlightingRule > mHighlightingRules
Definition: codeeditor.h:69
QRegularExpression mCommentEndExpression
Definition: codeeditor.h:73
void highlightBlock(const QString &text) override
Definition: codeeditor.cpp:219
QTextCharFormat mQuotationFormat
Definition: codeeditor.h:79
CodeEditorStyle * mWidgetStyle
Definition: codeeditor.h:82
void setStyle(const CodeEditorStyle &newStyle)
Definition: codeeditor.cpp:195
void setSymbols(const QStringList &symbols)
Definition: codeeditor.cpp:183
QTextCharFormat mMultiLineCommentFormat
Definition: codeeditor.h:78
QTextCharFormat mSymbolFormat
Definition: codeeditor.h:80
QVector< HighlightingRule > mHighlightingRulesWithSymbols
Definition: codeeditor.h:70
static int getPos(const QString &fileData, int lineNumber)
Definition: codeeditor.cpp:311
static const CodeEditorStyle defaultStyleLight(Qt::black, QColor(240, 240, 240), QColor(255, 220, 220), Qt::black, QColor(240, 240, 240), Qt::darkBlue, QFont::Bold, Qt::darkMagenta, QFont::Bold, Qt::darkGreen, QFont::Normal, Qt::gray, QFont::Normal, Qt::red, QColor(220, 220, 255), QFont::Normal)
@ style
Style warning.
QRegularExpression pattern
Definition: codeeditor.h:62