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