1 #include "ce_widget.h"
2 #include "AsmView.h"
3 #include "AsmViewModel.h"
4 #include "ce_plugin.h"
5 #include "ce_service.h"
6 #include "compiledbreader.h"
7
8 #include <QComboBox>
9 #include <QEvent>
10 #include <QHBoxLayout>
11 #include <QHoverEvent>
12 #include <QInputDialog>
13 #include <QJsonArray>
14 #include <QJsonDocument>
15 #include <QJsonObject>
16 #include <QLineEdit>
17 #include <QMenu>
18 #include <QPushButton>
19 #include <QSplitter>
20 #include <QStandardItemModel>
21 #include <QStringListModel>
22 #include <QToolButton>
23 #include <QTreeView>
24
25 #include <KConfigGroup>
26 #include <KLocalizedString>
27 #include <KMessageBox>
28 #include <KSharedConfig>
29 #include <KTextEditor/ConfigInterface>
30 #include <KTextEditor/MainWindow>
31 #include <KXMLGUIFactory>
32
33 enum CE_Options {
34 CE_Option_FilterLabel = 1,
35 CE_Option_IntelAsm,
36 CE_Option_FilterUnusedLibFuncs,
37 CE_Option_FilterComments,
38 CE_Option_Demangle,
39 };
40
readConfigForCEOption(CE_Options o)41 static bool readConfigForCEOption(CE_Options o)
42 {
43 KConfigGroup cg(KSharedConfig::openConfig(), "kate_compilerexplorer");
44 switch (o) {
45 case CE_Option_FilterLabel:
46 return cg.readEntry("FilterUnusedLabels", true);
47 case CE_Option_IntelAsm:
48 return cg.readEntry("UseIntelAsmSyntax", true);
49 case CE_Option_FilterUnusedLibFuncs:
50 return cg.readEntry("OptionFilterLibFuncs", true);
51 case CE_Option_FilterComments:
52 return cg.readEntry("OptionFilterComments", true);
53 case CE_Option_Demangle:
54 return cg.readEntry("OptionDemangle", true);
55 default:
56 Q_UNREACHABLE();
57 }
58 }
59
writeConfigForCEOption(CE_Options o,bool value)60 static void writeConfigForCEOption(CE_Options o, bool value)
61 {
62 KConfigGroup cg(KSharedConfig::openConfig(), "kate_compilerexplorer");
63 switch (o) {
64 case CE_Option_FilterLabel:
65 return cg.writeEntry("FilterUnusedLabels", value);
66 case CE_Option_IntelAsm:
67 return cg.writeEntry("UseIntelAsmSyntax", value);
68 case CE_Option_FilterUnusedLibFuncs:
69 return cg.writeEntry("OptionFilterLibFuncs", value);
70 case CE_Option_FilterComments:
71 return cg.writeEntry("OptionFilterComments", value);
72 case CE_Option_Demangle:
73 return cg.writeEntry("OptionDemangle", value);
74 default:
75 Q_UNREACHABLE();
76 }
77 }
78
CEWidget(CEPluginView * pluginView,KTextEditor::MainWindow * mainWindow)79 CEWidget::CEWidget(CEPluginView *pluginView, KTextEditor::MainWindow *mainWindow)
80 : QWidget() // The widget will be passed on to kate, no need for a parent
81 , m_pluginView(pluginView)
82 , m_mainWindow(mainWindow)
83 , m_asmView(new AsmView(this))
84 , m_model(new AsmViewModel(this))
85 , m_lineEdit(new QLineEdit(this))
86 , m_languagesCombo(new QComboBox(this))
87 , m_compilerCombo(new QComboBox(this))
88 , m_optsCombo(new QToolButton(this))
89 , m_compileButton(new QPushButton(this))
90 {
91 doc = m_mainWindow->activeView()->document();
92
93 setWindowTitle(QStringLiteral("Compiler Explorer - ") + doc->documentName());
94
95 auto mainLayout = new QVBoxLayout;
96 setLayout(mainLayout);
97
98 createTopBar(mainLayout);
99 createMainViews(mainLayout);
100
101 connect(m_compileButton, &QPushButton::clicked, this, &CEWidget::doCompile);
102 connect(CompilerExplorerSvc::instance(), &CompilerExplorerSvc::asmResult, this, &CEWidget::processAndShowAsm);
103
104 connect(this, &CEWidget::lineHovered, m_model, &AsmViewModel::highlightLinkedAsm);
105 connect(m_asmView, &AsmView::scrollToLineRequested, this, [this](int line) {
106 m_textEditor->setCursorPosition({line, 0});
107 });
108
109 QString file = doc->url().toLocalFile();
110 QString compilecmds = CompileDBReader::locateCompileCommands(m_mainWindow, file);
111 QString args = CompileDBReader::filteredArgsForFile(compilecmds, file);
112 m_lineEdit->setText(args);
113
114 warnIfBadArgs(args.split(QLatin1Char(' ')));
115
116 setFocusPolicy(Qt::StrongFocus);
117 }
118
~CEWidget()119 CEWidget::~CEWidget()
120 {
121 removeViewAsActiveXMLGuiClient();
122 }
123
shouldClose()124 bool CEWidget::shouldClose()
125 {
126 int ret = KMessageBox::warningYesNo(this, i18n("Do you really want to close %1?", windowTitle()));
127 return ret == KMessageBox::Yes;
128 }
129
removeViewAsActiveXMLGuiClient()130 void CEWidget::removeViewAsActiveXMLGuiClient()
131 {
132 if (m_textEditor) {
133 m_mainWindow->guiFactory()->removeClient(m_textEditor);
134 }
135
136 if (oldClient) {
137 m_mainWindow->guiFactory()->addClient(oldClient);
138 }
139 }
140
setViewAsActiveXMLGuiClient()141 void CEWidget::setViewAsActiveXMLGuiClient()
142 {
143 if (!m_textEditor) {
144 return;
145 }
146
147 // remove current active "KTE::View" client from mainWindow
148 const auto clients = m_mainWindow->guiFactory()->clients();
149 for (KXMLGUIClient *c : clients) {
150 if (c->componentName() == QStringLiteral("katepart") && c != m_textEditor) {
151 oldClient = static_cast<KTextEditor::View *>(c);
152 m_mainWindow->guiFactory()->removeClient(c);
153 break;
154 }
155 }
156 // Make us the client
157 m_mainWindow->guiFactory()->addClient(m_textEditor);
158 }
159
eventFilter(QObject * o,QEvent * e)160 bool CEWidget::eventFilter(QObject *o, QEvent *e)
161 {
162 // We live in a stacked widget in kateviewspace
163 // use hide/show to figure out when we are not active
164 if (e->type() == QEvent::Show) {
165 setViewAsActiveXMLGuiClient();
166 return QWidget::eventFilter(o, e);
167 } else if (e->type() == QEvent::Hide) {
168 removeViewAsActiveXMLGuiClient();
169 return QWidget::eventFilter(o, e);
170 }
171
172 if (o != m_textEditor) {
173 return QWidget::eventFilter(o, e);
174 }
175
176 if (e->type() != QEvent::HoverMove) {
177 return QWidget::eventFilter(o, e);
178 }
179
180 auto event = static_cast<QHoverEvent *>(e);
181 auto cursor = m_textEditor->coordinatesToCursor(event->pos());
182 Q_EMIT lineHovered(cursor.line()); // Can be invalid, that is okay
183 m_asmView->viewport()->update();
184
185 return QWidget::eventFilter(o, e);
186 }
187
createTopBar(QVBoxLayout * mainLayout)188 void CEWidget::createTopBar(QVBoxLayout *mainLayout)
189 {
190 QHBoxLayout *topBarLayout = new QHBoxLayout;
191 mainLayout->addLayout(topBarLayout);
192
193 topBarLayout->addWidget(m_languagesCombo);
194 topBarLayout->addWidget(m_compilerCombo);
195 topBarLayout->addWidget(m_optsCombo);
196 topBarLayout->addWidget(m_lineEdit);
197 topBarLayout->addWidget(m_compileButton);
198
199 auto svc = CompilerExplorerSvc::instance();
200
201 connect(svc, &CompilerExplorerSvc::languages, this, &CEWidget::setAvailableLanguages);
202 svc->sendRequest(CompilerExplorer::Languages);
203
204 connect(svc, &CompilerExplorerSvc::compilers, this, &CEWidget::setAvailableCompilers);
205 svc->sendRequest(CompilerExplorer::Compilers);
206
207 m_compileButton->setText(i18n("Compile"));
208
209 initOptionsComboBox();
210 }
211
setAvailableLanguages(const QByteArray & data)212 void CEWidget::setAvailableLanguages(const QByteArray &data)
213 {
214 if (!doc) {
215 return;
216 }
217
218 const QJsonArray langs = QJsonDocument::fromJson(data).array();
219
220 // We use the highlightingMode to set the current active language,
221 // but this needs some sort of mapping to CE's lang names
222 auto currentFileLang = doc->highlightingMode();
223 QString activeLang;
224
225 m_languagesCombo->clear();
226
227 for (const auto &langJV : langs) {
228 const auto lang = langJV.toObject();
229 const auto id = lang.value(QStringLiteral("id")).toString();
230 const auto name = lang.value(QStringLiteral("name")).toString();
231
232 if (name == currentFileLang) {
233 activeLang = name;
234 }
235
236 m_languagesCombo->addItem(name, id);
237 }
238 m_languagesCombo->setCurrentText(activeLang);
239 m_languagesCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
240
241 connect(m_languagesCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
242 QString id = m_languagesCombo->itemData(index).toString();
243 repopulateCompilersCombo(id);
244 });
245 }
246
setAvailableCompilers(const QByteArray & data)247 void CEWidget::setAvailableCompilers(const QByteArray &data)
248 {
249 if (!doc) {
250 return;
251 }
252
253 const QJsonArray json = QJsonDocument::fromJson(data).array();
254
255 m_langToCompiler.clear();
256
257 for (const auto &value : json) {
258 const auto compilerName = value[QStringLiteral("name")].toString();
259 const auto lang = value[QStringLiteral("lang")].toString();
260 const auto id = value[QStringLiteral("id")];
261
262 Compiler compiler{compilerName, id};
263 m_langToCompiler.push_back({lang, compiler});
264 }
265
266 repopulateCompilersCombo(doc->highlightingMode().toLower());
267 m_compilerCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
268 }
269
repopulateCompilersCombo(const QString & lang)270 void CEWidget::repopulateCompilersCombo(const QString &lang)
271 {
272 auto currentFileLang = lang;
273
274 auto compilersForLang = compilersForLanguage(currentFileLang);
275 if (compilersForLang.empty()) {
276 compilersForLang = m_langToCompiler;
277 }
278
279 m_compilerCombo->clear();
280
281 for (const auto &[lang, compiler] : compilersForLang) {
282 m_compilerCombo->addItem(compiler.name, compiler.id);
283 }
284
285 m_compilerCombo->setCurrentIndex(0);
286 }
287
compilersForLanguage(const QString & lang) const288 std::vector<CEWidget::CompilerLangPair> CEWidget::compilersForLanguage(const QString &lang) const
289 {
290 std::vector<CompilerLangPair> compilersForLang;
291 for (const auto &pair : m_langToCompiler) {
292 if (pair.first == lang) {
293 compilersForLang.push_back(pair);
294 }
295 }
296 return compilersForLang;
297 }
298
initOptionsComboBox()299 void CEWidget::initOptionsComboBox()
300 {
301 QMenu *menu = new QMenu(this);
302 m_optsCombo->setMenu(menu);
303 m_optsCombo->setToolButtonStyle(Qt::ToolButtonTextOnly);
304 m_optsCombo->setText(i18n("Options"));
305 m_optsCombo->setPopupMode(QToolButton::InstantPopup);
306 m_optsCombo->setArrowType(Qt::DownArrow);
307
308 auto checkableAction = [this](const QString &name, CE_Options o) {
309 QAction *action = new QAction(name, this);
310 action->setCheckable(true);
311 action->setChecked(readConfigForCEOption(o));
312 action->setData((int)o);
313
314 connect(action, &QAction::toggled, this, [o](bool v) {
315 writeConfigForCEOption(o, v);
316 });
317
318 return action;
319 };
320
321 menu->addAction(checkableAction(i18n("Demangle Identifiers"), CE_Option_Demangle));
322 menu->addAction(checkableAction(i18n("Filter Library Functions"), CE_Option_FilterUnusedLibFuncs));
323 menu->addAction(checkableAction(i18n("Filter Unused Labels"), CE_Option_FilterLabel));
324 menu->addAction(checkableAction(i18n("Filter Comments"), CE_Option_FilterComments));
325 menu->addAction(checkableAction(i18n("Intel Syntax"), CE_Option_IntelAsm));
326
327 menu->addAction(i18n("Change Url..."), this, [this] {
328 KConfigGroup cg(KSharedConfig::openConfig(), "kate_compilerexplorer");
329 QString url = cg.readEntry("kate_compilerexplorer_url", QStringLiteral("http://localhost:10240"));
330
331 bool ok = false;
332 QString newUrl = QInputDialog::getText(this,
333 i18n("Enter Url"),
334 i18n("Enter Url to CompilerExplorer instance. For e.g., http://localhost:10240"),
335 QLineEdit::Normal,
336 url,
337 &ok);
338
339 if (ok && !newUrl.isEmpty()) {
340 CompilerExplorerSvc::instance()->changeUrl(newUrl);
341 cg.writeEntry("kate_compilerexplorer_url", newUrl);
342 }
343 });
344 }
345
createMainViews(QVBoxLayout * mainLayout)346 void CEWidget::createMainViews(QVBoxLayout *mainLayout)
347 {
348 if (!doc) {
349 return;
350 }
351
352 QSplitter *splitter = new QSplitter(this);
353
354 m_textEditor = doc->createView(this, m_mainWindow);
355
356 setViewAsActiveXMLGuiClient();
357
358 m_asmView->setModel(m_model);
359
360 addExtraActionstoTextEditor();
361 m_textEditor->installEventFilter(this);
362 m_textEditor->focusProxy()->installEventFilter(this);
363
364 splitter->addWidget(m_textEditor);
365 splitter->addWidget(m_asmView);
366 splitter->setSizes({INT_MAX, INT_MAX});
367
368 mainLayout->addWidget(splitter);
369 }
370
currentCompiler() const371 QString CEWidget::currentCompiler() const
372 {
373 return m_compilerCombo->currentData().toString();
374 }
375
compilationFailed(const QJsonObject & obj)376 bool CEWidget::compilationFailed(const QJsonObject &obj)
377 {
378 int colWidth = m_asmView->columnWidth(AsmViewModel::Column_Text);
379
380 QFontMetrics fm(m_asmView->font());
381 int avgCharWidth = fm.averageCharWidth();
382
383 int maxChars = colWidth / avgCharWidth;
384
385 auto code = obj.value(QStringLiteral("code"));
386 if (!code.isUndefined()) {
387 int compilerReturnCode = code.toInt();
388 if (compilerReturnCode != 0) {
389 const auto stderror = obj.value(QStringLiteral("stderr")).toArray();
390 std::vector<AsmRow> rows;
391
392 // strip any escape sequences
393 static const QRegularExpression re(QStringLiteral("[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]"));
394
395 AsmRow r;
396 r.text = i18n("Compiler returned: %1", compilerReturnCode);
397 rows.push_back(r);
398
399 for (const auto &err : stderror) {
400 QString text = err.toObject().value(QStringLiteral("text")).toString();
401 text.replace(re, QLatin1String(""));
402
403 QStringList lines;
404 lines.reserve(text.size() / maxChars);
405 for (int i = 0; i < text.size(); i += maxChars) {
406 lines << text.mid(i, maxChars);
407 }
408
409 for (const auto &line : std::as_const(lines)) {
410 AsmRow r;
411 r.text = line;
412 rows.push_back(r);
413 }
414 }
415
416 m_model->setDataFromCE(std::move(rows), {}, {});
417 return true;
418 }
419 }
420 return false;
421 }
422
processAndShowAsm(const QByteArray & data)423 void CEWidget::processAndShowAsm(const QByteArray &data)
424 {
425 m_model->clear();
426
427 std::vector<AsmRow> rows;
428 QHash<SourcePos, AsmViewModel::CodeGenLines> sourceToAsm;
429
430 const auto json = QJsonDocument::fromJson(data);
431
432 const auto mainObj = json.object();
433
434 if (compilationFailed(mainObj)) {
435 m_model->setHasError(true);
436 return;
437 }
438 m_model->setHasError(false);
439
440 // printf("%s\n", json.toJson().constData());
441 const QJsonArray assembly = mainObj.value(QStringLiteral("asm")).toArray();
442 rows.reserve(assembly.size());
443
444 int currentAsmLine = 0;
445
446 // Process the assembly
447 for (const auto &line : assembly) {
448 AsmRow row;
449
450 auto labels = line[QStringLiteral("labels")].toArray();
451 if (!labels.empty()) {
452 for (const auto &label : labels) {
453 LabelInRow l;
454 auto rangeJV = label.toObject().value(QStringLiteral("range"));
455
456 const auto range = rangeJV.toObject();
457 int startCol = range.value(QStringLiteral("startCol")).toInt() - 1;
458 int endCol = range.value(QStringLiteral("endCol")).toInt();
459 l.col = startCol;
460 l.len = endCol - startCol;
461 row.labels.push_back(l);
462 }
463 }
464
465 const auto source = line[QStringLiteral("source")].toObject();
466 QString file = source.value(QStringLiteral("file")).toString();
467 int srcLine = source.value(QStringLiteral("line")).toInt();
468 int srcCol = source.value(QStringLiteral("column")).toInt();
469
470 row.source.file = file.isEmpty() ? QString() : file; // can be empty
471 row.source.line = srcLine;
472 row.source.col = srcCol;
473
474 row.text = line[QStringLiteral("text")].toString();
475
476 rows.push_back(row);
477 sourceToAsm[row.source].push_back(currentAsmLine);
478
479 currentAsmLine++;
480 }
481
482 const QJsonObject labelDefinitions = mainObj.value(QStringLiteral("labelDefinitions")).toObject();
483 // Label => Line Number
484 QHash<QString, int> labelDefs;
485 auto it = labelDefinitions.constBegin();
486 auto end = labelDefinitions.constEnd();
487 for (; it != end; ++it) {
488 labelDefs.insert(it.key(), it.value().toInt());
489 }
490
491 m_model->setDataFromCE(std::move(rows), std::move(sourceToAsm), std::move(labelDefs));
492 m_asmView->resizeColumnToContents(0);
493 }
494
doCompile()495 void CEWidget::doCompile()
496 {
497 m_model->clear();
498
499 if (!doc) {
500 return;
501 }
502
503 if (auto ciface = qobject_cast<KTextEditor::ConfigInterface *>(m_textEditor)) {
504 auto font = ciface->configValue(QStringLiteral("font")).value<QFont>();
505 m_model->setFont(font);
506 }
507
508 const QString text = doc->text();
509 if (text.isEmpty()) {
510 return;
511 }
512
513 const auto actions = m_optsCombo->menu()->actions();
514
515 bool demangle = false;
516 bool intel = false;
517 bool labels = false;
518 bool comments = false;
519 bool libfuncs = false;
520 for (auto action : actions) {
521 bool isChecked = action->isChecked();
522 if (action->data().toInt() == CE_Option_Demangle)
523 demangle = isChecked;
524 else if (action->data().toInt() == CE_Option_FilterComments)
525 comments = isChecked;
526 else if (action->data().toInt() == CE_Option_FilterLabel)
527 labels = isChecked;
528 else if (action->data().toInt() == CE_Option_FilterUnusedLibFuncs)
529 libfuncs = isChecked;
530 else if (action->data().toInt() == CE_Option_IntelAsm)
531 intel = isChecked;
532 }
533
534 QString args2 = m_lineEdit->text().trimmed();
535
536 const auto json = CompilerExplorerSvc::getCompilationOptions(text, args2, intel, demangle, labels, comments, libfuncs);
537 const auto compiler = currentCompiler();
538 const QString endpoint = QStringLiteral("compiler/") + compiler + QStringLiteral("/compile");
539
540 CompilerExplorerSvc::instance()->compileRequest(endpoint, json.toJson());
541 }
542
addExtraActionstoTextEditor()543 void CEWidget::addExtraActionstoTextEditor()
544 {
545 Q_ASSERT(m_textEditor);
546
547 auto m = m_textEditor->defaultContextMenu();
548
549 QMenu *menu = new QMenu(this);
550 menu->addAction(i18n("Reveal linked code"), this, [this] {
551 auto line = m_textEditor->cursorPosition().line();
552 SourcePos p{QString(), line + 1, 0};
553 AsmViewModel::CodeGenLines asmLines = m_model->asmForSourcePos(p);
554 // qDebug() << "Linked code for: " << line;
555 if (!asmLines.empty()) {
556 auto index = m_model->index(asmLines.front(), 0);
557 m_asmView->scrollTo(index, QAbstractItemView::PositionAtCenter);
558
559 // Highlight the linked lines
560 Q_EMIT lineHovered(line);
561 m_asmView->viewport()->update();
562 // qDebug() << "Scrolling to: " << index.data() << asmLines.size() << asmLines.front();
563 }
564 });
565 menu->addActions(m->actions());
566 m_textEditor->setContextMenu(menu);
567 }
568
sendMessage(const QString & plainText,bool warn)569 void CEWidget::sendMessage(const QString &plainText, bool warn)
570 {
571 // use generic output view
572 QVariantMap genericMessage;
573 genericMessage.insert(QStringLiteral("type"), warn ? QStringLiteral("Error") : QStringLiteral("Info"));
574 genericMessage.insert(QStringLiteral("category"), i18n("CompilerExplorer"));
575 genericMessage.insert(QStringLiteral("text"), plainText);
576 Q_EMIT m_pluginView->message(genericMessage);
577 }
578
warnIfBadArgs(const QStringList & args)579 void CEWidget::warnIfBadArgs(const QStringList &args)
580 {
581 QStringList warnableArgs = {QStringLiteral("flto"), QStringLiteral("fsanitize")};
582 QStringList found;
583 for (const auto &a : args) {
584 for (const auto &w : warnableArgs) {
585 if (a.contains(w)) {
586 warnableArgs.removeOne(w);
587 found.append(w);
588 }
589 }
590 }
591
592 QString msg = i18n("'%1' compiler flags were found. Output may not be useful.", found.join(QLatin1String(", ")));
593 sendMessage(msg, true);
594 }
595