Bug Summary

File:builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_code_editor.cpp
Warning:line 653, column 38
Called C++ object pointer is null

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-pc-linux-gnu -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name lua_debugger_code_editor.cpp -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=deadcode -analyzer-checker=cplusplus -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -mrelocation-model pic -pic-level 2 -fhalf-no-semantic-interposition -fno-delete-null-pointer-checks -mframe-pointer=all -relaxed-aliasing -fmath-errno -ffp-contract=on -fno-rounding-math -ffloat16-excess-precision=fast -fbfloat16-excess-precision=fast -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/builds/wireshark/wireshark/build -fcoverage-compilation-dir=/builds/wireshark/wireshark/build -resource-dir /usr/lib/llvm-22/lib/clang/22 -isystem /usr/include/glib-2.0 -isystem /usr/lib/x86_64-linux-gnu/glib-2.0/include -isystem /builds/wireshark/wireshark/build/ui/qt -isystem /builds/wireshark/wireshark/ui/qt -isystem /builds/wireshark/wireshark/ui/qt/lua_debugger -isystem /usr/include/x86_64-linux-gnu/qt6/QtConcurrent -isystem /usr/include/x86_64-linux-gnu/qt6 -isystem /usr/include/x86_64-linux-gnu/qt6/QtCore -isystem /usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++ -isystem /usr/include/x86_64-linux-gnu/qt6/QtNetwork -isystem /usr/include/x86_64-linux-gnu/qt6/QtPrintSupport -isystem /usr/include/x86_64-linux-gnu/qt6/QtGui -isystem /usr/include/x86_64-linux-gnu/qt6/QtWidgets -isystem /usr/include/x86_64-linux-gnu/qt6/QtSvg -isystem /usr/include/x86_64-linux-gnu/qt6/QtMultimedia -isystem /usr/include/x86_64-linux-gnu/qt6/QtDBus -D CARES_NO_DEPRECATED -D G_DISABLE_DEPRECATED -D G_DISABLE_SINGLE_INCLUDES -D QT_CONCURRENT_LIB -D QT_CORE_LIB -D QT_DBUS_LIB -D QT_GUI_LIB -D QT_MULTIMEDIA_LIB -D QT_NETWORK_LIB -D QT_PRINTSUPPORT_LIB -D QT_SVG_LIB -D QT_WIDGETS_LIB -D WS_DEBUG -D WS_DEBUG_UTF_8 -I /builds/wireshark/wireshark/build/ui/qt/qtui_autogen/include -I /builds/wireshark/wireshark/build -I /builds/wireshark/wireshark -I /builds/wireshark/wireshark/include -D _GLIBCXX_ASSERTIONS -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/x86_64-linux-gnu/c++/16 -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/16/../../../../include/c++/16/backward -internal-isystem /usr/lib/llvm-22/lib/clang/22/include -internal-isystem /usr/local/include -internal-isystem /usr/lib/gcc/x86_64-linux-gnu/16/../../../../x86_64-linux-gnu/include -internal-externc-isystem /usr/include/x86_64-linux-gnu -internal-externc-isystem /include -internal-externc-isystem /usr/include -fmacro-prefix-map=/builds/wireshark/wireshark/= -fmacro-prefix-map=/builds/wireshark/wireshark/build/= -fmacro-prefix-map=../= -Wno-format-nonliteral -std=c++17 -fdeprecated-macro -ferror-limit 19 -fwrapv -fwrapv-pointer -fstrict-flex-arrays=3 -stack-protector 2 -fstack-clash-protection -fcf-protection=full -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -fcolor-diagnostics -analyzer-output=html -faddrsig -fdwarf2-cfi-asm -o /builds/wireshark/wireshark/sbout/2026-11-04-100346-3662-1 -x c++ /builds/wireshark/wireshark/ui/qt/lua_debugger/lua_debugger_code_editor.cpp
1/* lua_debugger_code_editor.cpp
2 *
3 * Wireshark - Network traffic analyzer
4 * By Gerald Combs <gerald@wireshark.org>
5 * Copyright 1998 Gerald Combs
6 *
7 * SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10/**
11 * @file
12 * Code editor stack: editor view, gutter, syntax highlighter, theme
13 * palette, font policy and the script-tabs controller.
14 */
15
16#include <lua_debugger_code_editor.h>
17#include <ui_lua_debugger_dialog.h>
18
19#include <QAction>
20#include <QApplication>
21#include <QColor>
22#include <QContextMenuEvent>
23#include <QDir>
24#include <QEvent>
25#include <QFile>
26#include <QFileInfo>
27#include <QFont>
28#include <QFontDatabase>
29#include <QGuiApplication>
30#include <QHeaderView>
31#include <QKeyEvent>
32#include <QList>
33#include <QMessageBox>
34#include <QMetaObject>
35#include <QModelIndex>
36#include <QMouseEvent>
37#include <QPainter>
38#include <QPalette>
39#include <QPlainTextEdit>
40#include <QStandardItem>
41#include <QStandardItemModel>
42#include <QStandardPaths>
43#include <QStringList>
44#include <QTabWidget>
45#include <QTextBlock>
46#include <QTextDocument>
47#include <QTextStream>
48#include <QTreeView>
49#include <QWidget>
50#include <QtGlobal>
51
52#include <algorithm>
53#include <climits>
54
55#include "epan/wslua/wslua_debugger.h"
56#include "lua_debugger_breakpoints.h"
57#include "lua_debugger_dialog.h"
58#include "lua_debugger_files.h"
59#include "main_application.h"
60#include <ui/qt/utils/font_manager.h>
61#include "utils/color_utils.h"
62#include <ui/qt/utils/theme_manager.h>
63
64/* ===== code_palette ===== */
65
66LuaDebuggerEditorPalette luaDebuggerEditorPalette()
67{
68 const ThemeManager *theme = ThemeManager::instance();
69 LuaDebuggerEditorPalette p;
70 p.editorBackground = theme->color(ThemeManager::CodeBackground);
71 p.editorText = theme->color(ThemeManager::CodeText);
72 p.selection = theme->color(ThemeManager::CodeSelection);
73 p.selectionText = theme->color(ThemeManager::CodeSelectionText);
74 p.gutterBackground = theme->color(ThemeManager::CodeGutter);
75 p.gutterText = theme->color(ThemeManager::CodeGutterText);
76 p.pausedLine = theme->color(ThemeManager::CodePausedLine);
77 return p;
78}
79
80/* ===== lua_highlighter ===== */
81
82
83namespace
84{
85
86constexpr qint32 kStringStateBase = 0x1000; // Unique positive range for long strings
87constexpr qint32 kCommentStateBase = 0x2000; // Distinct positive range for block comments
88
89} // namespace
90
91LuaSyntaxHighlighter::LuaSyntaxHighlighter(QTextDocument *parent) : QSyntaxHighlighter(parent)
92{
93 singleLineComment_ = QRegularExpression(QStringLiteral(R"(--(?!\[).*)")(QString(QtPrivate::qMakeStringPrivate(u"" R"(--(?!\[).*)"))));
94 buildRules();
95}
96
97void LuaSyntaxHighlighter::updateColors()
98{
99 buildRules();
100 rehighlight();
101}
102
103void LuaSyntaxHighlighter::buildRules()
104{
105 const ThemeManager *theme = ThemeManager::instance();
106 rules_.clear();
107
108 stringFormat_.setForeground(theme->color(ThemeManager::CodeString));
109 rules_.append({QRegularExpression(QStringLiteral("\"(?:\\\\.|[^\"\\\\])*\"")(QString(QtPrivate::qMakeStringPrivate(u"" "\"(?:\\\\.|[^\"\\\\])*\""
)))
), stringFormat_});
110 rules_.append({QRegularExpression(QStringLiteral("'(?:\\\\.|[^'\\\\])*'")(QString(QtPrivate::qMakeStringPrivate(u"" "'(?:\\\\.|[^'\\\\])*'"
)))
), stringFormat_});
111
112 QTextCharFormat keywordFormat;
113 keywordFormat.setForeground(theme->color(ThemeManager::CodeKeyword));
114 keywordFormat.setFontWeight(QFont::Bold);
115 const QStringList keywords = {
116 QStringLiteral("and")(QString(QtPrivate::qMakeStringPrivate(u"" "and"))), QStringLiteral("break")(QString(QtPrivate::qMakeStringPrivate(u"" "break"))), QStringLiteral("do")(QString(QtPrivate::qMakeStringPrivate(u"" "do"))), QStringLiteral("else")(QString(QtPrivate::qMakeStringPrivate(u"" "else"))),
117 QStringLiteral("elseif")(QString(QtPrivate::qMakeStringPrivate(u"" "elseif"))), QStringLiteral("end")(QString(QtPrivate::qMakeStringPrivate(u"" "end"))), QStringLiteral("false")(QString(QtPrivate::qMakeStringPrivate(u"" "false"))), QStringLiteral("for")(QString(QtPrivate::qMakeStringPrivate(u"" "for"))),
118 QStringLiteral("function")(QString(QtPrivate::qMakeStringPrivate(u"" "function"))), QStringLiteral("goto")(QString(QtPrivate::qMakeStringPrivate(u"" "goto"))), QStringLiteral("if")(QString(QtPrivate::qMakeStringPrivate(u"" "if"))), QStringLiteral("in")(QString(QtPrivate::qMakeStringPrivate(u"" "in"))),
119 QStringLiteral("local")(QString(QtPrivate::qMakeStringPrivate(u"" "local"))), QStringLiteral("nil")(QString(QtPrivate::qMakeStringPrivate(u"" "nil"))), QStringLiteral("not")(QString(QtPrivate::qMakeStringPrivate(u"" "not"))), QStringLiteral("or")(QString(QtPrivate::qMakeStringPrivate(u"" "or"))),
120 QStringLiteral("repeat")(QString(QtPrivate::qMakeStringPrivate(u"" "repeat"))), QStringLiteral("return")(QString(QtPrivate::qMakeStringPrivate(u"" "return"))), QStringLiteral("then")(QString(QtPrivate::qMakeStringPrivate(u"" "then"))), QStringLiteral("true")(QString(QtPrivate::qMakeStringPrivate(u"" "true"))),
121 QStringLiteral("until")(QString(QtPrivate::qMakeStringPrivate(u"" "until"))), QStringLiteral("while")(QString(QtPrivate::qMakeStringPrivate(u"" "while")))};
122 for (const QString &keyword : keywords)
123 {
124 Rule rule;
125 rule.pattern = QRegularExpression(QStringLiteral("\\b%1\\b")(QString(QtPrivate::qMakeStringPrivate(u"" "\\b%1\\b"))).arg(keyword));
126 rule.format = keywordFormat;
127 rules_.append(rule);
128 }
129
130 QTextCharFormat numberFormat;
131 numberFormat.setForeground(theme->color(ThemeManager::CodeNumber));
132 rules_.append({QRegularExpression(QStringLiteral("\\b0[xX][0-9a-fA-F]+\\b")(QString(QtPrivate::qMakeStringPrivate(u"" "\\b0[xX][0-9a-fA-F]+\\b"
)))
), numberFormat});
133 rules_.append({QRegularExpression(QStringLiteral("\\b\\d+(?:\\.\\d+)?\\b")(QString(QtPrivate::qMakeStringPrivate(u"" "\\b\\d+(?:\\.\\d+)?\\b"
)))
), numberFormat});
134
135 QTextCharFormat builtInFormat;
136 builtInFormat.setForeground(theme->color(ThemeManager::CodeBuiltIn));
137 rules_.append({QRegularExpression(QStringLiteral("\\bself\\b")(QString(QtPrivate::qMakeStringPrivate(u"" "\\bself\\b")))), builtInFormat});
138
139 QTextCharFormat functionCallFormat;
140 functionCallFormat.setForeground(theme->color(ThemeManager::CodeFunctionCall));
141 Rule functionCallRule;
142 functionCallRule.pattern = QRegularExpression(QStringLiteral("\\b(?!function\\b)([A-Za-z_]\\w*)\\s*(?=\\()")(QString(QtPrivate::qMakeStringPrivate(u"" "\\b(?!function\\b)([A-Za-z_]\\w*)\\s*(?=\\()"
)))
);
143 functionCallRule.format = functionCallFormat;
144 rules_.append(functionCallRule);
145
146 commentFormat_.setForeground(theme->color(ThemeManager::CodeComment));
147}
148
149void LuaSyntaxHighlighter::highlightBlock(const QString &text)
150{
151 for (const Rule &rule : rules_)
152 {
153 QRegularExpressionMatchIterator iterator = rule.pattern.globalMatch(text);
154 while (iterator.hasNext())
155 {
156 const QRegularExpressionMatch match = iterator.next();
157 setFormat(static_cast<int>(match.capturedStart()), static_cast<int>(match.capturedLength()), rule.format);
158 }
159 }
160
161 QRegularExpressionMatchIterator commentMatches = singleLineComment_.globalMatch(text);
162 while (commentMatches.hasNext())
163 {
164 const QRegularExpressionMatch match = commentMatches.next();
165 setFormat(static_cast<int>(match.capturedStart()), static_cast<int>(match.capturedLength()), commentFormat_);
166 }
167
168 const qint32 previousState = previousBlockState();
169 const bool continuingString = (previousState >= kStringStateBase) && (previousState < kCommentStateBase);
170 const bool continuingComment = previousState >= kCommentStateBase;
171 const qint32 stringEqCount = continuingString ? (previousState - kStringStateBase) : 0;
172 const qint32 commentEqCount = continuingComment ? (previousState - kCommentStateBase) : 0;
173
174 qint32 nextStringEqCount = 0;
175 const bool stringStillOpen = highlightLongBlock(text, false, continuingString, stringEqCount, nextStringEqCount);
176
177 qint32 nextCommentEqCount = 0;
178 const bool commentStillOpen = highlightLongBlock(text, true, continuingComment, commentEqCount, nextCommentEqCount);
179
180 if (commentStillOpen)
181 {
182 setCurrentBlockState(kCommentStateBase + nextCommentEqCount);
183 }
184 else if (stringStillOpen)
185 {
186 setCurrentBlockState(kStringStateBase + nextStringEqCount);
187 }
188 else
189 {
190 setCurrentBlockState(0);
191 }
192}
193
194bool LuaSyntaxHighlighter::highlightLongBlock(const QString &text, bool isComment, bool continuingPrevious,
195 qint32 eqCountFromState, qint32 &nextStateEqCount)
196{
197 QTextCharFormat format = isComment ? commentFormat_ : stringFormat_;
198 qint32 eqCount = eqCountFromState;
199 qint32 tokenLength = 0;
200 bool continuing = continuingPrevious;
201 qint32 startIndex = continuing ? 0 : findLongBlockStart(text, 0, isComment, eqCount, tokenLength);
202
203 while (startIndex >= 0)
204 {
205 const qint32 searchFrom = continuing ? 0 : startIndex + tokenLength;
206 const qint32 endIndex = findLongBlockEnd(text, searchFrom, eqCount);
207 if (endIndex == -1)
208 {
209 setFormat(startIndex, static_cast<int>(text.length()) - startIndex, format);
210 nextStateEqCount = eqCount;
211 return true;
212 }
213
214 setFormat(startIndex, endIndex - startIndex + 1, format);
215 continuing = false;
216 startIndex = findLongBlockStart(text, endIndex + 1, isComment, eqCount, tokenLength);
217 }
218
219 nextStateEqCount = 0;
220 return false;
221}
222
223qint32 LuaSyntaxHighlighter::findLongBlockStart(const QString &text, qint32 from, bool isComment, qint32 &eqCount,
224 qint32 &tokenLength) const
225{
226 const qint32 len = static_cast<qint32>(text.length());
227 for (qint32 i = from; i < len; ++i)
228 {
229 if (isComment)
230 {
231 if (i + 3 >= len)
232 {
233 break;
234 }
235 if (text[i] == '-' && text[i + 1] == '-' && text[i + 2] == '[')
236 {
237 qint32 j = i + 3;
238 qint32 equals = 0;
239 while (j < len && text[j] == '=')
240 {
241 ++equals;
242 ++j;
243 }
244 if (j < len && text[j] == '[')
245 {
246 eqCount = equals;
247 tokenLength = (j - i) + 1;
248 return i;
249 }
250 }
251 }
252 else
253 {
254 if (text[i] == '[')
255 {
256 if (i >= 2 && text[i - 2] == '-' && text[i - 1] == '-')
257 {
258 continue; // Part of comment start, skip
259 }
260 qint32 j = i + 1;
261 qint32 equals = 0;
262 while (j < len && text[j] == '=')
263 {
264 ++equals;
265 ++j;
266 }
267 if (j < len && text[j] == '[')
268 {
269 eqCount = equals;
270 tokenLength = (j - i) + 1;
271 return i;
272 }
273 }
274 }
275 }
276 return -1;
277}
278
279qint32 LuaSyntaxHighlighter::findLongBlockEnd(const QString &text, qint32 from, qint32 eqCount) const
280{
281 const qint32 len = static_cast<qint32>(text.length());
282 for (qint32 i = from; i < len; ++i)
283 {
284 if (text[i] == ']')
285 {
286 qint32 j = i + 1;
287 qint32 equals = 0;
288 while (j < len && text[j] == '=')
289 {
290 ++equals;
291 ++j;
292 }
293 if (equals == eqCount && j < len && text[j] == ']')
294 {
295 return j;
296 }
297 }
298 }
299 return -1;
300}
301
302/* ===== code_view ===== */
303
304LuaDebuggerCodeView::LuaDebuggerCodeView(QWidget *parent)
305 : QPlainTextEdit(parent), lineNumberArea(new LineNumberArea(this)), syntaxHighlighter(nullptr)
306{
307 /* Gutter tooltip surfaces both click affordances; the Shift+click
308 * behavior is otherwise non-discoverable. */
309 lineNumberArea->setToolTip(tr("Click: add or remove breakpoint\n"
310 "Drag existing breakpoint: move to nearest free visible line\n"
311 "Shift+click: on an empty line, add a disabled breakpoint;\n"
312 "on an existing breakpoint, toggle its active state"));
313 syntaxHighlighter = new LuaSyntaxHighlighter(document());
314
315 connect(this, &LuaDebuggerCodeView::blockCountChanged, this, &LuaDebuggerCodeView::updateLineNumberAreaWidth);
316 connect(this, &LuaDebuggerCodeView::updateRequest, this, &LuaDebuggerCodeView::updateLineNumberArea);
317 connect(this, &LuaDebuggerCodeView::cursorPositionChanged, this, &LuaDebuggerCodeView::rebuildLineHighlights);
318
319 /* QAbstractScrollArea delivers key events to the viewport; Esc never
320 * reaches QDialog::keyPressEvent. Forward to LuaDebuggerDialog::handleEscapeKey(). */
321 viewport()->installEventFilter(this);
322
323 setReadOnly(false);
324 setLineWrapMode(QPlainTextEdit::NoWrap);
325
326 QFont initialFont;
327 if (mainApp && mainApp->isInitialized())
328 {
329 initialFont = FontManager::monospaceFont();
330 }
331 setEditorFont(initialFont);
332 applyEditorPalette();
333 rebuildLineHighlights();
334}
335
336QString LuaDebuggerCodeView::luaIdentifierUnderCursor(const QTextCursor &cursor) const
337{
338 const QString block = cursor.block().text();
339 const int posInBlock = cursor.positionInBlock();
340 if (block.isEmpty() || posInBlock < 0 || posInBlock > block.size())
341 {
342 return {};
343 }
344
345 auto isIdentChar = [](QChar c) { return c.isLetterOrNumber() || c == QLatin1Char('_'); };
346 auto isIdentStart = [](QChar c) { return c.isLetter() || c == QLatin1Char('_'); };
347
348 /* Caret may sit one past the end of the identifier; expand left
349 * until we are inside, then sweep both directions. */
350 int start = posInBlock;
351 int end = posInBlock;
352 if (start > 0 && !isIdentChar(block.at(start - 1)) && (start >= block.size() || !isIdentChar(block.at(start))))
353 {
354 return {};
355 }
356 while (start > 0 && isIdentChar(block.at(start - 1)))
357 {
358 --start;
359 }
360 while (end < block.size() && isIdentChar(block.at(end)))
361 {
362 ++end;
363 }
364 if (start >= end)
365 {
366 return {};
367 }
368 if (!isIdentStart(block.at(start)))
369 {
370 return {};
371 }
372 return block.mid(start, end - start);
373}
374
375QString LuaDebuggerCodeView::watchExpressionForContextMenu(const QPoint &viewportPos) const
376{
377 QString s = textCursor().selectedText().trimmed();
378 if (!s.isEmpty())
379 {
380 return s;
381 }
382 return luaIdentifierUnderCursor(cursorForPosition(viewportPos));
383}
384
385qint32 LuaDebuggerCodeView::lineNumberAreaWidth()
386{
387 qint32 digits = 1;
388 qint32 maxBlockCount = qMax(1, blockCount());
389 while (maxBlockCount >= 10)
390 {
391 maxBlockCount /= 10;
392 ++digits;
393 }
394
395 const qint32 space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits;
396 return space + 20; // Extra space for breakpoint icon
397}
398
399void LuaDebuggerCodeView::updateLineNumberAreaWidth(int /* newBlockCount */)
400{
401 setViewportMargins(static_cast<int>(lineNumberAreaWidth()), 0, 0, 0);
402}
403
404void LuaDebuggerCodeView::updateLineNumberArea(const QRect &rect, int dy)
405{
406 if (dy)
407 lineNumberArea->scroll(0, dy);
408 else
409 lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
410
411 if (rect.contains(viewport()->rect()))
412 updateLineNumberAreaWidth(0);
413}
414
415void LuaDebuggerCodeView::resizeEvent(QResizeEvent *e)
416{
417 QPlainTextEdit::resizeEvent(e);
418
419 QRect cr = contentsRect();
420 lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), static_cast<int>(lineNumberAreaWidth()), cr.height()));
421}
422
423bool LuaDebuggerCodeView::eventFilter(QObject *watched, QEvent *event)
424{
425 if (watched == viewport() && event->type() == QEvent::KeyPress)
426 {
427 auto *ke = static_cast<QKeyEvent *>(event);
428 if (ke->key() == Qt::Key_Escape && ke->modifiers() == Qt::NoModifier)
429 {
430 if (LuaDebuggerDialog *dlg = LuaDebuggerDialog::instance())
431 {
432 QMetaObject::invokeMethod(dlg, "handleEscapeKey", Qt::QueuedConnection);
433 return true;
434 }
435 }
436 }
437 return QPlainTextEdit::eventFilter(watched, event);
438}
439
440void LuaDebuggerCodeView::rebuildLineHighlights()
441{
442 QList<QTextEdit::ExtraSelection> extraSelections;
443
444 /* Debugger paused line — amber bar; theme-specific (independent of caret). */
445 if (pausedExecutionLine_ > 0)
446 {
447 QTextBlock pauseBlock = document()->findBlockByNumber(static_cast<int>(pausedExecutionLine_ - 1));
448 if (pauseBlock.isValid())
449 {
450 QTextCursor pauseCursor(pauseBlock);
451 pauseCursor.movePosition(QTextCursor::StartOfBlock);
452 QTextEdit::ExtraSelection pauseSel;
453 const LuaDebuggerEditorPalette palette = luaDebuggerEditorPalette();
454 pauseSel.format.setBackground(palette.pausedLine);
455 pauseSel.format.setProperty(QTextFormat::FullWidthSelection, true);
456 pauseSel.cursor = pauseCursor;
457 pauseSel.cursor.clearSelection();
458 extraSelections.append(pauseSel);
459 }
460 }
461
462 /* Caret line — subtle; skip if same line as debugger (do not replace pause look). */
463 QTextBlock caretBlock = textCursor().block();
464 if (caretBlock.isValid())
465 {
466 const int caretLine = caretBlock.blockNumber() + 1;
467 const bool sameAsPause = (pausedExecutionLine_ > 0) && (caretLine == pausedExecutionLine_);
468
469 if (!sameAsPause)
470 {
471 /* Match the line-number gutter background (see applyEditorPalette). */
472 const QColor lineColor = lineNumberArea->palette().color(QPalette::Base);
473
474 QTextCursor caretLineCursor(caretBlock);
475 caretLineCursor.movePosition(QTextCursor::StartOfBlock);
476 QTextEdit::ExtraSelection caretSel;
477 caretSel.format.setBackground(lineColor);
478 caretSel.format.setProperty(QTextFormat::FullWidthSelection, true);
479 caretSel.cursor = caretLineCursor;
480 caretSel.cursor.clearSelection();
481 extraSelections.append(caretSel);
482 }
483 }
484
485 setExtraSelections(extraSelections);
486}
487
488void LuaDebuggerCodeView::setCurrentLine(qint32 line)
489{
490 /* The gutter repaints on cursor movement via the updateRequest
491 * signal, but the early-return branches below do not move the
492 * cursor, and we need the paused-line yellow arrow (and the
493 * accompanying dimmed breakpoint circle, if any) to appear /
494 * disappear whenever pausedExecutionLine_ changes. Request a
495 * line-number-area repaint explicitly in every branch. */
496 if (line <= 0)
497 {
498 pausedExecutionLine_ = -1;
499 rebuildLineHighlights();
500 lineNumberArea->update();
501 return;
502 }
503
504 QTextBlock block = document()->findBlockByNumber(static_cast<int>(line - 1));
505 if (!block.isValid())
506 {
507 pausedExecutionLine_ = -1;
508 rebuildLineHighlights();
509 lineNumberArea->update();
510 return;
511 }
512
513 pausedExecutionLine_ = line;
514 QTextCursor cursor(block);
515 cursor.movePosition(QTextCursor::StartOfBlock);
516 setTextCursor(cursor);
517 rebuildLineHighlights();
518 lineNumberArea->update();
519}
520
521void LuaDebuggerCodeView::moveCaretToLineStart(qint32 line)
522{
523 if (line <= 0)
524 {
525 return;
526 }
527
528 QTextBlock block = document()->findBlockByNumber(static_cast<int>(line - 1));
529 if (!block.isValid())
530 {
531 return;
532 }
533
534 QTextCursor cursor(block);
535 cursor.movePosition(QTextCursor::StartOfBlock);
536 setTextCursor(cursor);
537}
538
539void LuaDebuggerCodeView::clearCurrentLineHighlight()
540{
541 pausedExecutionLine_ = -1;
542 rebuildLineHighlights();
543 /* Force the gutter to repaint so the paused-line yellow arrow
544 * is cleared and any dimmed breakpoint circle returns to full
545 * brightness on resume, even when no cursor movement triggers
546 * updateRequest. */
547 lineNumberArea->update();
548}
549
550void LuaDebuggerCodeView::setEditorFont(const QFont &font)
551{
552 QFont resolvedFont = font;
553 if (resolvedFont.family().isEmpty())
554 {
555 resolvedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
556 }
557 resolvedFont.setStyleHint(QFont::TypeWriter, QFont::PreferDefault);
558 QPlainTextEdit::setFont(resolvedFont);
559 lineNumberArea->setFont(resolvedFont);
560 updateLineNumberAreaWidth(0);
561 lineNumberArea->update();
562 viewport()->update();
563}
564
565void LuaDebuggerCodeView::updateBreakpointMarkers()
566{
567 lineNumberArea->update();
568}
569
570void LuaDebuggerCodeView::applyTheme()
571{
572 applyEditorPalette();
573 if (syntaxHighlighter)
574 {
575 static_cast<LuaSyntaxHighlighter *>(syntaxHighlighter)->updateColors();
576 }
577 lineNumberArea->update();
578 viewport()->update();
579 rebuildLineHighlights();
580}
581
582void LuaDebuggerCodeView::applyEditorPalette()
583{
584 const LuaDebuggerEditorPalette colors = luaDebuggerEditorPalette();
585 QPalette pal = palette();
586 QPalette gutterPal = lineNumberArea->palette();
587
588 pal.setColor(QPalette::Base, colors.editorBackground);
589 pal.setColor(QPalette::Text, colors.editorText);
590 pal.setColor(QPalette::Highlight, colors.selection);
591 pal.setColor(QPalette::HighlightedText, colors.selectionText);
592 /* Match the editor base on Window so the gap between gutter and
593 * viewport doesn't reveal the host palette. */
594 pal.setColor(QPalette::Window, colors.editorBackground);
595
596 gutterPal.setColor(QPalette::Window, colors.gutterBackground);
597 gutterPal.setColor(QPalette::Base, colors.gutterBackground);
598 gutterPal.setColor(QPalette::WindowText, colors.gutterText);
599
600 setPalette(pal);
601 setAutoFillBackground(true);
602
603 viewport()->setPalette(pal);
604 viewport()->setAutoFillBackground(true);
605
606 lineNumberArea->setPalette(gutterPal);
607 lineNumberArea->setAutoFillBackground(true);
608 lineNumberArea->update();
609}
610
611void LuaDebuggerCodeView::lineNumberAreaPaintEvent(QPaintEvent *event)
612{
613 const LuaDebuggerEditorPalette colors = luaDebuggerEditorPalette();
614 QPainter painter(lineNumberArea);
615 painter.setRenderHint(QPainter::Antialiasing, true);
616
617 painter.fillRect(event->rect(), colors.gutterBackground);
618
619 /* Highlight the drag target line if a breakpoint move is in progress. */
620 LineNumberArea *gutter = static_cast<LineNumberArea *>(lineNumberArea);
621 if (gutter && gutter->isDraggingBreakpoint() && gutter->dragTargetLine() > 0)
1
Assuming 'gutter' is null
622 {
623 const qint32 targetLine = gutter->dragTargetLine();
624 QTextBlock targetBlock = document()->findBlockByNumber(static_cast<int>(targetLine - 1));
625 if (targetBlock.isValid())
626 {
627 qint32 targetTop = static_cast<qint32>(blockBoundingGeometry(targetBlock).translated(contentOffset()).top());
628 qint32 targetHeight = static_cast<qint32>(blockBoundingRect(targetBlock).height());
629 QColor dragHighlight = colors.selection;
630 dragHighlight.setAlpha(80);
631 painter.fillRect(0, targetTop, lineNumberArea->width(), targetHeight, dragHighlight);
632 }
633 }
634
635 /* Canonicalize the filename once for all visible lines. */
636 char *canonical = nullptr;
637 if (!filename.isEmpty())
2
Assuming the condition is false
3
Taking false branch
638 {
639 canonical = wslua_debugger_canonical_path(filename.toUtf8().constData());
640 }
641
642 QTextBlock block = firstVisibleBlock();
643 qint32 blockNumber = block.blockNumber();
644 qint32 top = static_cast<qint32>(blockBoundingGeometry(block).translated(contentOffset()).top());
645 qint32 bottom = top + static_cast<qint32>(blockBoundingRect(block).height());
646
647 while (block.isValid() && top <= event->rect().bottom())
4
Assuming the condition is true
5
Assuming the condition is true
648 {
649 if (block.isVisible() && bottom >= event->rect().top())
6
Assuming the condition is true
7
Assuming the condition is true
8
Taking true branch
650 {
651 QString number = QString::number(blockNumber + 1);
652 painter.setPen(colors.gutterText);
653 painter.drawText(0, top, lineNumberArea->width() - 20, fontMetrics().height(), Qt::AlignRight, number);
9
Called C++ object pointer is null
654
655 const qint32 lineNo = static_cast<qint32>(blockNumber + 1);
656 const bool pausedHere = (pausedExecutionLine_ == lineNo);
657
658 /* Breakpoint circle. On the paused line the red (enabled)
659 * circle is drawn dimmed (reduced alpha) so the overlaid
660 * yellow right-pointing triangle is visually dominant while
661 * the breakpoint itself stays recognizable underneath.
662 * Gray (disabled) circles are not dimmed — the yellow
663 * arrow is the only indicator that is never dimmed.
664 * During a breakpoint drag, skip drawing the source line's
665 * breakpoint so the preview at the target is visually distinct. */
666 const bool isDragSource = (gutter && gutter->isDraggingBreakpoint() && lineNo == gutter->dragSourceLine());
667 if (canonical && !isDragSource)
668 {
669 bool hasExtras = false;
670 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, lineNo, &hasExtras);
671 if (state != -1)
672 {
673 const qint32 radius = fontMetrics().height() / 2 - 2;
674 const qint32 cx = lineNumberArea->width() - 15;
675 const qint32 cy = top + 3;
676 luaDbgDrawBreakpointDot(painter, cx, cy, radius, (state == 1), hasExtras);
677 }
678 }
679
680 /* Yellow right-pointing triangle marks the line the
681 * debugger is paused at. Drawn after (and therefore on
682 * top of) the breakpoint circle, which is dimmed on this
683 * line so the triangle dominates while the breakpoint is
684 * still visible. */
685 if (pausedHere)
686 {
687 const qint32 radius = fontMetrics().height() / 2 - 2;
688 const qint32 diameter = radius * 2;
689 const qreal x = lineNumberArea->width() - 15;
690 const qreal y = top + 2;
691 QPolygonF triangle;
692 triangle << QPointF(x, y) << QPointF(x, y + diameter) << QPointF(x + diameter, y + diameter / 2.0);
693 const QColor triangleColor("#FFC107");
694 painter.setBrush(triangleColor);
695 /* Same 1px darker rim as the toolbar state indicator
696 * (see updateEnabledCheckboxIcon()). */
697 painter.setPen(QPen(triangleColor.darker(140), 1));
698 painter.drawPolygon(triangle);
699 }
700 }
701
702 block = block.next();
703 top = bottom;
704 bottom = top + static_cast<qint32>(blockBoundingRect(block).height());
705 ++blockNumber;
706 }
707
708 /* Draw semi-transparent preview breakpoint at the drag target line.
709 * Uses the same color and indicators (white core for extras) as the
710 * source breakpoint, with reduced alpha for visual distinction. */
711 if (gutter && gutter->isDraggingBreakpoint() && gutter->dragTargetLine() > 0)
712 {
713 const qint32 targetLine = gutter->dragTargetLine();
714 const qint32 sourceLine = gutter->dragSourceLine();
715 QTextBlock targetBlock = document()->findBlockByNumber(static_cast<int>(targetLine - 1));
716 if (targetBlock.isValid() && canonical && sourceLine > 0)
717 {
718 /* Query the source breakpoint to get its state and extras info. */
719 bool hasExtras = false;
720 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, sourceLine, &hasExtras);
721 if (state != -1)
722 {
723 qint32 targetTop = static_cast<qint32>(blockBoundingGeometry(targetBlock).translated(contentOffset()).top());
724 qint32 targetBottom = targetTop + static_cast<qint32>(blockBoundingRect(targetBlock).height());
725
726 if (targetTop <= event->rect().bottom() && targetBottom >= event->rect().top())
727 {
728 const qint32 radius = fontMetrics().height() / 2 - 2;
729 const qint32 cx = lineNumberArea->width() - 15;
730 const qint32 cy = targetTop + 3;
731 luaDbgDrawBreakpointDot(painter, cx, cy, radius, (state == 1), hasExtras, 120);
732 }
733 }
734 }
735 }
736
737 g_free(canonical)(__builtin_object_size ((canonical), 0) != ((size_t) - 1)) ? g_free_sized
(canonical, __builtin_object_size ((canonical), 0)) : (g_free
) (canonical)
;
738}
739
740qint32 LineNumberArea::lineAtY(qint32 yPx) const
741{
742 QTextBlock block = codeEditor->firstVisibleBlock();
743 qint32 top =
744 static_cast<qint32>(codeEditor->blockBoundingGeometry(block).translated(codeEditor->contentOffset()).top());
745 qint32 bottom = top + static_cast<qint32>(codeEditor->blockBoundingRect(block).height());
746 qint32 blockNumber = block.blockNumber();
747 while (block.isValid())
748 {
749 if (yPx >= top && yPx <= bottom)
750 {
751 return blockNumber + 1;
752 }
753 block = block.next();
754 top = bottom;
755 bottom = top + static_cast<qint32>(codeEditor->blockBoundingRect(block).height());
756 ++blockNumber;
757 }
758 return -1;
759}
760
761bool LineNumberArea::hasBreakpointAtLine(qint32 line) const
762{
763 if (!codeEditor || codeEditor->filename.isEmpty() || line < 1)
764 {
765 return false;
766 }
767
768 char *canonical = wslua_debugger_canonical_path(codeEditor->filename.toUtf8().constData());
769 if (!canonical)
770 {
771 return false;
772 }
773 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, line, /*has_extras=*/nullptr);
774 g_free(canonical)(__builtin_object_size ((canonical), 0) != ((size_t) - 1)) ? g_free_sized
(canonical, __builtin_object_size ((canonical), 0)) : (g_free
) (canonical)
;
775 return state != -1;
776}
777
778qint32 LineNumberArea::nearestVisibleDropLine(qint32 yPx, qint32 sourceLine) const
779{
780 struct Candidate
781 {
782 qint32 line;
783 qint32 centerY;
784 };
785
786 QVector<Candidate> candidates;
787 QTextBlock block = codeEditor->firstVisibleBlock();
788 qint32 top =
789 static_cast<qint32>(codeEditor->blockBoundingGeometry(block).translated(codeEditor->contentOffset()).top());
790 qint32 bottom = top + static_cast<qint32>(codeEditor->blockBoundingRect(block).height());
791 qint32 blockNumber = block.blockNumber();
792 const qint32 viewportBottom = codeEditor->viewport()->height();
793
794 while (block.isValid() && top <= viewportBottom)
795 {
796 if (block.isVisible() && bottom >= 0)
797 {
798 const qint32 line = blockNumber + 1;
799 const bool occupied = hasBreakpointAtLine(line);
800 if (!occupied || line == sourceLine)
801 {
802 candidates.append({line, top + (bottom - top) / 2});
803 }
804 }
805
806 block = block.next();
807 top = bottom;
808 bottom = top + static_cast<qint32>(codeEditor->blockBoundingRect(block).height());
809 ++blockNumber;
810 }
811
812 if (candidates.isEmpty())
813 {
814 return -1;
815 }
816
817 qint32 bestLine = -1;
818 qint32 bestDist = INT_MAX2147483647;
819 qint32 bestLineDelta = INT_MAX2147483647;
820 for (const Candidate &candidate : candidates)
821 {
822 const qint32 yDist = qAbs(candidate.centerY - yPx);
823 const qint32 lineDelta = qAbs(candidate.line - sourceLine);
824 if (bestLine == -1 || yDist < bestDist || (yDist == bestDist && lineDelta < bestLineDelta))
825 {
826 bestLine = candidate.line;
827 bestDist = yDist;
828 bestLineDelta = lineDelta;
829 }
830 }
831 return bestLine;
832}
833
834void LineNumberArea::mousePressEvent(QMouseEvent *event)
835{
836 leftPressArmed_ = false;
837 draggingBreakpoint_ = false;
838 pressedLine_ = -1;
839 dragTargetLine_ = -1;
840 pressShiftToggle_ = false;
841 pressHadBreakpoint_ = false;
842
843 /* Only the primary button drives add / remove / toggle here. The
844 * secondary button (right-click on Win/Linux, Ctrl-click or
845 * two-finger trackpad tap on macOS) is handled by
846 * @ref contextMenuEvent, which always pops the
847 * Edit / Disable / Remove menu so that gesture is never confused
848 * with the toggle path. */
849 if (event->button() != Qt::LeftButton)
850 {
851 QWidget::mousePressEvent(event);
852 return;
853 }
854
855 const QPoint click_pos = event->pos();
856 if (click_pos.x() <= width() - 20)
857 {
858 QWidget::mousePressEvent(event);
859 return;
860 }
861
862 const qint32 lineNo = lineAtY(click_pos.y());
863 if (lineNo < 1)
864 {
865 QWidget::mousePressEvent(event);
866 return;
867 }
868
869 leftPressArmed_ = true;
870 pressPos_ = click_pos;
871 pressedLine_ = lineNo;
872 dragTargetLine_ = lineNo;
873 pressShiftToggle_ = (event->modifiers() & Qt::ShiftModifier) != 0;
874 pressHadBreakpoint_ = hasBreakpointAtLine(lineNo);
875 event->accept();
876}
877
878void LineNumberArea::mouseMoveEvent(QMouseEvent *event)
879{
880 if (!leftPressArmed_ || !pressHadBreakpoint_)
881 {
882 QWidget::mouseMoveEvent(event);
883 return;
884 }
885
886 if (!draggingBreakpoint_)
887 {
888 const qint32 dragDistance = (event->pos() - pressPos_).manhattanLength();
889 if (dragDistance < QApplication::startDragDistance())
890 {
891 return;
892 }
893 draggingBreakpoint_ = true;
894 setCursor(Qt::ClosedHandCursor);
895 }
896
897 const qint32 prevTarget = dragTargetLine_;
898 dragTargetLine_ = nearestVisibleDropLine(event->pos().y(), pressedLine_);
899
900 /* Repaint gutter to show updated drag target highlight. */
901 if (dragTargetLine_ != prevTarget)
902 {
903 update();
904 }
905 event->accept();
906}
907
908void LineNumberArea::mouseReleaseEvent(QMouseEvent *event)
909{
910 if (event->button() != Qt::LeftButton)
911 {
912 QWidget::mouseReleaseEvent(event);
913 return;
914 }
915
916 const bool armed = leftPressArmed_;
917 const bool wasDragging = draggingBreakpoint_;
918 const bool sourceHasBreakpoint = pressHadBreakpoint_;
919 const bool toggleActive = pressShiftToggle_;
920 const qint32 lineNo = pressedLine_;
921 const qint32 dropLine = (dragTargetLine_ > 0) ? dragTargetLine_ : nearestVisibleDropLine(event->pos().y(), lineNo);
922
923 leftPressArmed_ = false;
924 draggingBreakpoint_ = false;
925 pressedLine_ = -1;
926 dragTargetLine_ = -1;
927 pressShiftToggle_ = false;
928 pressHadBreakpoint_ = false;
929 unsetCursor();
930
931 if (!armed || lineNo < 1)
932 {
933 QWidget::mouseReleaseEvent(event);
934 return;
935 }
936
937 if (wasDragging && sourceHasBreakpoint)
938 {
939 if (dropLine > 0 && dropLine != lineNo)
940 {
941 emit codeEditor->breakpointMoveRequested(codeEditor->filename, lineNo, dropLine);
942 codeEditor->viewport()->update();
943 update();
944 }
945 event->accept();
946 return;
947 }
948
949 /* Plain left-click on a "rich" breakpoint (one carrying a
950 * condition, a hit-count target, or a log message) opens the
951 * Edit / Disable / Remove popup instead of removing it: those
952 * extras are easy to lose to a misclick, so the destructive
953 * action requires an explicit Remove choice from the menu. Plain
954 * breakpoints and clicks on bare lines keep the original
955 * add-or-remove-on-click flow. Shift+click keeps the existing
956 * modifier semantics (toggle active / pre-arm disabled). */
957 bool richBp = false;
958 if (!toggleActive && !codeEditor->filename.isEmpty())
959 {
960 char *canonical = wslua_debugger_canonical_path(codeEditor->filename.toUtf8().constData());
961 if (canonical)
962 {
963 bool hasExtras = false;
964 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, lineNo, &hasExtras);
965 g_free(canonical)(__builtin_object_size ((canonical), 0) != ((size_t) - 1)) ? g_free_sized
(canonical, __builtin_object_size ((canonical), 0)) : (g_free
) (canonical)
;
966 if (state != -1 && hasExtras)
967 {
968 richBp = true;
969 }
970 }
971 }
972
973 if (richBp)
974 {
975 QPoint globalPos = event->globalPosition().toPoint();
976 emit codeEditor->breakpointGutterMenuRequested(codeEditor->filename, lineNo, globalPos);
977 }
978 else
979 {
980 emit codeEditor->breakpointToggled(codeEditor->filename, lineNo, toggleActive);
981 codeEditor->viewport()->update();
982 update();
983 }
984
985 event->accept();
986}
987
988void LineNumberArea::contextMenuEvent(QContextMenuEvent *event)
989{
990 /* Secondary-click contract on the gutter: always pop the
991 * Edit / Disable / Remove menu when the gesture lands on an
992 * existing breakpoint, regardless of whether it carries extras.
993 * This is the canonical "second click" affordance — works on
994 * Windows / Linux right-click, macOS Ctrl-click and the macOS
995 * two-finger trackpad tap, all of which Qt funnels through
996 * QContextMenuEvent. Bare lines have nothing to act on; we let
997 * the event through so the parent editor (or its viewport) can
998 * decide what to do (currently nothing in that area). */
999 const QPoint pos = event->pos();
1000 if (pos.x() <= width() - 20 || codeEditor->filename.isEmpty())
1001 {
1002 QWidget::contextMenuEvent(event);
1003 return;
1004 }
1005
1006 const qint32 lineNo = lineAtY(pos.y());
1007 if (lineNo < 1)
1008 {
1009 QWidget::contextMenuEvent(event);
1010 return;
1011 }
1012
1013 char *canonical = wslua_debugger_canonical_path(codeEditor->filename.toUtf8().constData());
1014 if (!canonical)
1015 {
1016 QWidget::contextMenuEvent(event);
1017 return;
1018 }
1019 const int32_t state = wslua_debugger_get_breakpoint_state_canonical(canonical, lineNo, /*has_extras=*/nullptr);
1020 g_free(canonical)(__builtin_object_size ((canonical), 0) != ((size_t) - 1)) ? g_free_sized
(canonical, __builtin_object_size ((canonical), 0)) : (g_free
) (canonical)
;
1021 if (state == -1)
1022 {
1023 QWidget::contextMenuEvent(event);
1024 return;
1025 }
1026
1027 emit codeEditor->breakpointGutterMenuRequested(codeEditor->filename, lineNo, event->globalPos());
1028 event->accept();
1029}
1030
1031/* ===== font_policy ===== */
1032
1033void LuaDebuggerFontPolicy::attach(QTabWidget *codeTabs, QTreeView *variablesTree, QTreeView *watchTree,
1034 QStandardItemModel *watchModel, QTreeView *stackTree, QTreeView *fileTree,
1035 QTreeView *breakpointsTree, QPlainTextEdit *evalInputEdit,
1036 QPlainTextEdit *evalOutputEdit)
1037{
1038 codeTabs_ = codeTabs;
1039 variablesTree_ = variablesTree;
1040 watchTree_ = watchTree;
1041 watchModel_ = watchModel;
1042 stackTree_ = stackTree;
1043 fileTree_ = fileTree;
1044 breakpointsTree_ = breakpointsTree;
1045 evalInputEdit_ = evalInputEdit;
1046 evalOutputEdit_ = evalOutputEdit;
1047}
1048
1049void LuaDebuggerFontPolicy::applyAll()
1050{
1051 applyToCodeEditors(monospaceFont(true));
1052 applyToPanels();
1053}
1054
1055void LuaDebuggerFontPolicy::applyToCodeEditors(const QFont &font)
1056{
1057 QFont effective = font;
1058 if (effective.family().isEmpty())
1059 {
1060 effective = monospaceFont(true);
1061 }
1062
1063 if (!codeTabs_)
1064 {
1065 return;
1066 }
1067 const qint32 tabCount = static_cast<qint32>(codeTabs_->count());
1068 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1069 {
1070 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(codeTabs_->widget(static_cast<int>(tabIndex)));
1071 if (view)
1072 {
1073 view->setEditorFont(effective);
1074 }
1075 }
1076}
1077
1078void LuaDebuggerFontPolicy::applyToPanels()
1079{
1080 const QFont panelMono = monospaceFont(false);
1081 const QFont headerFont = regularFont();
1082
1083 const QList<QWidget *> widgets = {variablesTree_, watchTree_, stackTree_,
1084 breakpointsTree_, evalInputEdit_, evalOutputEdit_};
1085 for (QWidget *widget : widgets)
1086 {
1087 if (widget)
1088 {
1089 widget->setFont(panelMono);
1090 }
1091 }
1092
1093 const QList<QTreeView *> treesWithStandardHeaders = {variablesTree_, watchTree_, stackTree_, fileTree_,
1094 breakpointsTree_};
1095 for (QTreeView *tree : treesWithStandardHeaders)
1096 {
1097 if (tree && tree->header())
1098 {
1099 tree->header()->setFont(headerFont);
1100 }
1101 }
1102 reapplyToWatchItemModel();
1103}
1104
1105namespace
1106{
1107
1108/* Walk the watch QStandardItemModel tree: set each item's QFont to the
1109 * panel monospace while preserving the existing bold bit (change-
1110 * highlight) so it wins over the tree widget font after row moves. */
1111// NOLINTNEXTLINE(misc-no-recursion)
1112void reapplyMonospaceToWatchItemModelRecursive(const QFont &base, QStandardItemModel *m, const QModelIndex &parent)
1113{
1114 if (!m)
1115 {
1116 return;
1117 }
1118 const int rows = m->rowCount(parent);
1119 const int cols = m->columnCount(parent);
1120 for (int r = 0; r < rows; ++r)
1121 {
1122 for (int c = 0; c < cols; ++c)
1123 {
1124 const QModelIndex idx = m->index(r, c, parent);
1125 if (QStandardItem *it = m->itemFromIndex(idx))
1126 {
1127 QFont f = base;
1128 f.setBold(it->font().bold());
1129 it->setFont(f);
1130 }
1131 }
1132 const QModelIndex specIndex = m->index(r, WatchColumn::Spec, parent);
1133 if (specIndex.isValid() && m->rowCount(specIndex) > 0)
1134 {
1135 reapplyMonospaceToWatchItemModelRecursive(base, m, specIndex);
1136 }
1137 }
1138}
1139
1140} // namespace
1141
1142void LuaDebuggerFontPolicy::reapplyToWatchItemModel()
1143{
1144 if (!watchModel_)
1145 {
1146 return;
1147 }
1148 reapplyMonospaceToWatchItemModelRecursive(monospaceFont(false), watchModel_, QModelIndex());
1149 if (watchTree_)
1150 {
1151 watchTree_->update();
1152 }
1153}
1154
1155// TODO: LuaDebuggerFontPolicy duplicates FontManager (font resolution + the
1156// app-ready fallback). Drop it and have callers use the FontManager directly.
1157QFont LuaDebuggerFontPolicy::monospaceFont(bool zoomed) const
1158{
1159 /* Monospace font for panels and the script editor. */
1160 if (mainApp && mainApp->isInitialized())
1161 {
1162 return (zoomed ? FontManager::zoomedMonospaceFont() : FontManager::monospaceFont());
1163 }
1164
1165 /* Fall back to system fixed font */
1166 return QFontDatabase::systemFont(QFontDatabase::FixedFont);
1167}
1168
1169QFont LuaDebuggerFontPolicy::regularFont() const
1170{
1171 if (mainApp && mainApp->isInitialized())
1172 {
1173 return mainApp->font();
1174 }
1175 return QGuiApplication::font();
1176}
1177
1178/* ===== code_tabs_controller ===== */
1179
1180LuaDebuggerCodeTabsController::LuaDebuggerCodeTabsController(LuaDebuggerDialog *host) : QObject(host), host_(host) {}
1181
1182void LuaDebuggerCodeTabsController::attach(QTabWidget *tabs)
1183{
1184 tabs_ = tabs;
1185}
1186
1187void LuaDebuggerDialog::wireCodeTabs()
1188{
1189 codeTabsController_.attach(ui->codeTabWidget);
1190
1191 connect(ui->actionSaveFile, &QAction::triggered, &codeTabsController_, &LuaDebuggerCodeTabsController::onSaveFile);
1192
1193 connect(ui->codeTabWidget, &QTabWidget::tabCloseRequested, &codeTabsController_,
1194 &LuaDebuggerCodeTabsController::onTabCloseRequested);
1195 connect(ui->codeTabWidget, &QTabWidget::currentChanged, &codeTabsController_,
1196 &LuaDebuggerCodeTabsController::onCurrentTabChanged);
1197}
1198
1199QString LuaDebuggerCodeTabsController::lastOpenDirectory()
1200{
1201 if (lastOpenDirectory_.isEmpty())
1202 {
1203 lastOpenDirectory_ = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
1204 if (lastOpenDirectory_.isEmpty())
1205 {
1206 lastOpenDirectory_ = QDir::homePath();
1207 }
1208 }
1209 return lastOpenDirectory_;
1210}
1211
1212void LuaDebuggerCodeTabsController::setLastOpenDirectory(const QString &dir)
1213{
1214 lastOpenDirectory_ = dir;
1215}
1216
1217LuaDebuggerCodeView *LuaDebuggerCodeTabsController::loadFile(const QString &file_path)
1218{
1219 if (!host_ || !tabs_)
1220 {
1221 return nullptr;
1222 }
1223
1224 QString normalizedPath = host_->normalizedFilePath(file_path);
1225 if (normalizedPath.isEmpty())
1226 {
1227 normalizedPath = file_path;
1228 }
1229
1230 /* Check if file exists before creating a tab. */
1231 QFileInfo fileInfo(normalizedPath);
1232 if (!fileInfo.exists() || !fileInfo.isFile())
1233 {
1234 return nullptr;
1235 }
1236
1237 const qint32 existingTabCount = static_cast<qint32>(tabs_->count());
1238 for (qint32 tabIndex = 0; tabIndex < existingTabCount; ++tabIndex)
1239 {
1240 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1241 if (view && view->getFilename() == normalizedPath)
1242 {
1243 tabs_->setCurrentIndex(static_cast<int>(tabIndex));
1244 return view;
1245 }
1246 }
1247
1248 LuaDebuggerCodeView *codeView = new LuaDebuggerCodeView(tabs_);
1249 codeView->setEditorFont(host_->fontPolicy().monospaceFont(true));
1250 codeView->setFilename(normalizedPath);
1251
1252 QFile file(normalizedPath);
1253 if (file.open(QIODevice::ReadOnly | QIODevice::Text))
1254 {
1255 codeView->setPlainText(file.readAll());
1256 }
1257 else
1258 {
1259 /* This should not happen since we checked exists() above,
1260 * but handle it gracefully just in case. */
1261 delete codeView;
1262 return nullptr;
1263 }
1264
1265 host_->filesController().ensureEntry(normalizedPath);
1266
1267 codeView->setContextMenuPolicy(Qt::CustomContextMenu);
1268 connect(codeView, &QWidget::customContextMenuRequested, host_, &LuaDebuggerDialog::onCodeViewContextMenu);
1269
1270 /* Queued connection: the gutter emits this from inside its
1271 * mousePressEvent, so dispatching the popup via a queued slot
1272 * lets the press event fully unwind before QMenu::exec() spins
1273 * its own nested loop. Avoids the "press grab still attached"
1274 * artefacts that bite when a modal popup is opened directly out
1275 * of a press handler. */
1276 connect(codeView, &LuaDebuggerCodeView::breakpointGutterMenuRequested, &host_->breakpointsController(),
1277 &LuaDebuggerBreakpointsController::showGutterMenu, Qt::QueuedConnection);
1278
1279 connect(codeView, &LuaDebuggerCodeView::breakpointToggled, this,
1280 &LuaDebuggerCodeTabsController::onCodeViewBreakpointToggled);
1281
1282 connect(codeView, &LuaDebuggerCodeView::breakpointMoveRequested, &host_->breakpointsController(),
1283 &LuaDebuggerBreakpointsController::moveAtLine, Qt::QueuedConnection);
1284
1285 connect(codeView->document(), &QTextDocument::modificationChanged, this,
1286 [this, codeView]()
1287 {
1288 updateTabTextForCodeView(codeView);
1289 updateWindowModifiedState();
1290 if (tabs_ && tabs_->currentWidget() == codeView)
1291 {
1292 updateSaveActionState();
1293 }
1294 });
1295 connect(codeView, &QPlainTextEdit::cursorPositionChanged, &host_->breakpointsController(),
1296 &LuaDebuggerBreakpointsController::updateHeaderButtonState);
1297
1298 tabs_->addTab(codeView, QFileInfo(normalizedPath).fileName());
1299 updateTabTextForCodeView(codeView);
1300 tabs_->setCurrentWidget(codeView);
1301 tabs_->show();
1302 updateSaveActionState();
1303 updateWindowModifiedState();
1304 host_->updateLuaEditorAuxFrames();
1305 return codeView;
1306}
1307
1308LuaDebuggerCodeView *LuaDebuggerCodeTabsController::currentCodeView() const
1309{
1310 if (!tabs_)
1311 {
1312 return nullptr;
1313 }
1314 return qobject_cast<LuaDebuggerCodeView *>(tabs_->currentWidget());
1315}
1316
1317qint32 LuaDebuggerCodeTabsController::unsavedOpenScriptTabCount() const
1318{
1319 if (!tabs_)
1320 {
1321 return 0;
1322 }
1323 qint32 count = 0;
1324 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1325 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1326 {
1327 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1328 if (view && view->document()->isModified())
1329 {
1330 ++count;
1331 }
1332 }
1333 return count;
1334}
1335
1336bool LuaDebuggerCodeTabsController::hasUnsavedChanges() const
1337{
1338 return unsavedOpenScriptTabCount() > 0;
1339}
1340
1341bool LuaDebuggerCodeTabsController::ensureUnsavedChangesHandled(const QString &title)
1342{
1343 if (!hasUnsavedChanges())
1344 {
1345 return true;
1346 }
1347
1348 const qint32 unsavedCount = unsavedOpenScriptTabCount();
1349 const QMessageBox::StandardButton reply =
1350 QMessageBox::question(host_, title, tr("There are unsaved changes in %Ln open file(s).", "", unsavedCount),
1351 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save);
1352
1353 if (reply == QMessageBox::Cancel)
1354 {
1355 return false;
1356 }
1357 if (reply == QMessageBox::Save)
1358 {
1359 return saveAllModified();
1360 }
1361 clearAllDocumentModified();
1362 return true;
1363}
1364
1365void LuaDebuggerCodeTabsController::clearAllDocumentModified()
1366{
1367 if (!tabs_)
1368 {
1369 return;
1370 }
1371 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1372 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1373 {
1374 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1375 if (view)
1376 {
1377 view->document()->setModified(false);
1378 }
1379 }
1380}
1381
1382bool LuaDebuggerCodeTabsController::saveCodeView(LuaDebuggerCodeView *view)
1383{
1384 if (!view)
1385 {
1386 return false;
1387 }
1388 const QString path = view->getFilename();
1389 if (path.isEmpty())
1390 {
1391 return false;
1392 }
1393
1394 QFile file(path);
1395 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
1396 {
1397 QMessageBox::warning(host_, tr("Save Lua Script"),
1398 tr("Could not write to %1:\n%2").arg(path, file.errorString()));
1399 return false;
1400 }
1401 QTextStream out(&file);
1402 out << view->toPlainText();
1403 file.close();
1404 view->document()->setModified(false);
1405 return true;
1406}
1407
1408bool LuaDebuggerCodeTabsController::saveAllModified()
1409{
1410 if (!tabs_)
1411 {
1412 return true;
1413 }
1414 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1415 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1416 {
1417 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1418 if (view && view->document()->isModified())
1419 {
1420 if (!saveCodeView(view))
1421 {
1422 return false;
1423 }
1424 }
1425 }
1426 return true;
1427}
1428
1429void LuaDebuggerCodeTabsController::updateTabTextForCodeView(LuaDebuggerCodeView *view)
1430{
1431 if (!view || !tabs_)
1432 {
1433 return;
1434 }
1435 const int tabIndex = tabs_->indexOf(view);
1436 if (tabIndex < 0)
1437 {
1438 return;
1439 }
1440 QString label = QFileInfo(view->getFilename()).fileName();
1441 if (view->document()->isModified())
1442 {
1443 label += QStringLiteral(" *")(QString(QtPrivate::qMakeStringPrivate(u"" " *")));
1444 }
1445 tabs_->setTabText(tabIndex, label);
1446}
1447
1448void LuaDebuggerCodeTabsController::updateSaveActionState()
1449{
1450 if (!host_)
1451 {
1452 return;
1453 }
1454 LuaDebuggerCodeView *view = currentCodeView();
1455 host_->setSaveActionEnabled(view && view->document()->isModified());
1456}
1457
1458void LuaDebuggerCodeTabsController::updateWindowModifiedState()
1459{
1460 if (!host_)
1461 {
1462 return;
1463 }
1464 host_->setWindowModified(hasUnsavedChanges());
1465}
1466
1467void LuaDebuggerCodeTabsController::openInitialBreakpointFiles(const QVector<QString> &files)
1468{
1469 for (const QString &path : files)
1470 {
1471 loadFile(path);
1472 }
1473}
1474
1475void LuaDebuggerCodeTabsController::clearAllCodeHighlights()
1476{
1477 if (!tabs_)
1478 {
1479 return;
1480 }
1481 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1482 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1483 {
1484 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1485 if (view)
1486 {
1487 view->clearCurrentLineHighlight();
1488 }
1489 }
1490}
1491
1492void LuaDebuggerCodeTabsController::applyThemeToAllTabs()
1493{
1494 if (!tabs_)
1495 {
1496 return;
1497 }
1498 const qint32 tabCount = static_cast<qint32>(tabs_->count());
1499 for (qint32 tabIndex = 0; tabIndex < tabCount; ++tabIndex)
1500 {
1501 LuaDebuggerCodeView *view = qobject_cast<LuaDebuggerCodeView *>(tabs_->widget(static_cast<int>(tabIndex)));
1502 if (view)
1503 {
1504 view->applyTheme();
1505 }
1506 }
1507}
1508
1509void LuaDebuggerCodeTabsController::onSaveFile()
1510{
1511 LuaDebuggerCodeView *view = currentCodeView();
1512 if (!view || !view->document()->isModified())
1513 {
1514 return;
1515 }
1516 saveCodeView(view);
1517 updateSaveActionState();
1518}
1519
1520void LuaDebuggerCodeTabsController::onCurrentTabChanged(int index)
1521{
1522 Q_UNUSED(index)(void)index;;
1523 if (!host_)
1524 {
1525 return;
1526 }
1527 updateSaveActionState();
1528 host_->updateLuaEditorAuxFrames();
1529 host_->breakpointsController().updateHeaderButtonState();
1530 host_->updateContinueActionState();
1531}
1532
1533void LuaDebuggerCodeTabsController::onTabCloseRequested(int index)
1534{
1535 if (!host_ || !tabs_)
1536 {
1537 return;
1538 }
1539 QWidget *widget = tabs_->widget(index);
1540 auto *view = qobject_cast<LuaDebuggerCodeView *>(widget);
1541 if (view && view->document()->isModified())
1542 {
1543 const QMessageBox::StandardButton reply = QMessageBox::question(
1544 host_, tr("Lua Debugger"),
1545 tr("Save changes to %1 before closing?").arg(QFileInfo(view->getFilename()).fileName()),
1546 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel, QMessageBox::Save);
1547 if (reply == QMessageBox::Cancel)
1548 {
1549 return;
1550 }
1551 if (reply == QMessageBox::Save)
1552 {
1553 if (!saveCodeView(view))
1554 {
1555 return;
1556 }
1557 }
1558 else
1559 {
1560 view->document()->setModified(false);
1561 }
1562 }
1563
1564 tabs_->removeTab(index);
1565 delete widget;
1566 updateSaveActionState();
1567 updateWindowModifiedState();
1568}
1569
1570void LuaDebuggerCodeTabsController::onCodeViewBreakpointToggled(const QString &file_path, qint32 line, bool toggleActive)
1571{
1572 if (!host_)
1573 {
1574 return;
1575 }
1576 /* Two distinct gestures share this code-view signal:
1577 *
1578 * - Plain click (toggleActive=false) — F9 / gutter add+remove. The
1579 * breakpoint is created active and the debugger is auto-enabled
1580 * so the new row is immediately effective.
1581 * - Shift+click (toggleActive=true) — pre-arm / disarm without
1582 * destroying the row, so the user can park breakpoints on lines
1583 * they intend to revisit later. Never auto-enables the debugger;
1584 * that would defeat the "no surprise" intent.
1585 *
1586 * Both gestures dispatch through @ref LuaDebuggerBreakpointsController
1587 * so the chrome refresh (table rebuild, gutter markers, header dot)
1588 * is identical regardless of the entry point. */
1589 if (toggleActive)
1590 {
1591 host_->breakpointsController().shiftToggleAtLine(file_path, line);
1592 }
1593 else
1594 {
1595 host_->breakpointsController().toggleAtLine(file_path, line);
1596 }
1597}