File indexing completed on 2024-04-28 05:48:20

0001 #include "ce_widget.h"
0002 #include "AsmView.h"
0003 #include "AsmViewModel.h"
0004 #include "ce_plugin.h"
0005 #include "ce_service.h"
0006 #include "compiledbreader.h"
0007 #include "ktexteditor_utils.h"
0008 
0009 #include <QComboBox>
0010 #include <QEvent>
0011 #include <QHBoxLayout>
0012 #include <QHoverEvent>
0013 #include <QInputDialog>
0014 #include <QJsonArray>
0015 #include <QJsonDocument>
0016 #include <QJsonObject>
0017 #include <QLineEdit>
0018 #include <QMenu>
0019 #include <QPushButton>
0020 #include <QSplitter>
0021 #include <QToolButton>
0022 #include <QTreeView>
0023 
0024 #include <KConfigGroup>
0025 #include <KLocalizedString>
0026 #include <KMessageBox>
0027 #include <KSharedConfig>
0028 #include <KTextEditor/MainWindow>
0029 #include <KXMLGUIFactory>
0030 #include <kwidgetsaddons_version.h>
0031 
0032 enum CE_Options {
0033     CE_Option_FilterLabel = 1,
0034     CE_Option_IntelAsm,
0035     CE_Option_FilterUnusedLibFuncs,
0036     CE_Option_FilterComments,
0037     CE_Option_Demangle,
0038 };
0039 
0040 static bool readConfigForCEOption(CE_Options o)
0041 {
0042     KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("kate_compilerexplorer"));
0043     switch (o) {
0044     case CE_Option_FilterLabel:
0045         return cg.readEntry("FilterUnusedLabels", true);
0046     case CE_Option_IntelAsm:
0047         return cg.readEntry("UseIntelAsmSyntax", true);
0048     case CE_Option_FilterUnusedLibFuncs:
0049         return cg.readEntry("OptionFilterLibFuncs", true);
0050     case CE_Option_FilterComments:
0051         return cg.readEntry("OptionFilterComments", true);
0052     case CE_Option_Demangle:
0053         return cg.readEntry("OptionDemangle", true);
0054     default:
0055         Q_UNREACHABLE();
0056     }
0057 }
0058 
0059 static void writeConfigForCEOption(CE_Options o, bool value)
0060 {
0061     KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("kate_compilerexplorer"));
0062     switch (o) {
0063     case CE_Option_FilterLabel:
0064         return cg.writeEntry("FilterUnusedLabels", value);
0065     case CE_Option_IntelAsm:
0066         return cg.writeEntry("UseIntelAsmSyntax", value);
0067     case CE_Option_FilterUnusedLibFuncs:
0068         return cg.writeEntry("OptionFilterLibFuncs", value);
0069     case CE_Option_FilterComments:
0070         return cg.writeEntry("OptionFilterComments", value);
0071     case CE_Option_Demangle:
0072         return cg.writeEntry("OptionDemangle", value);
0073     default:
0074         Q_UNREACHABLE();
0075     }
0076 }
0077 
0078 CEWidget::CEWidget(CEPluginView *pluginView, KTextEditor::MainWindow *mainWindow)
0079     : QWidget() // The widget will be passed on to kate, no need for a parent
0080     , m_pluginView(pluginView)
0081     , m_mainWindow(mainWindow)
0082     , m_asmView(new AsmView(this))
0083     , m_model(new AsmViewModel(this))
0084     , m_lineEdit(new QLineEdit(this))
0085     , m_languagesCombo(new QComboBox(this))
0086     , m_compilerCombo(new QComboBox(this))
0087     , m_optsCombo(new QToolButton(this))
0088     , m_compileButton(new QPushButton(this))
0089 {
0090     doc = m_mainWindow->activeView()->document();
0091 
0092     setWindowTitle(QStringLiteral("Compiler Explorer - ") + doc->documentName());
0093 
0094     auto mainLayout = new QVBoxLayout;
0095     setLayout(mainLayout);
0096 
0097     createTopBar(mainLayout);
0098     createMainViews(mainLayout);
0099 
0100     connect(m_compileButton, &QPushButton::clicked, this, &CEWidget::doCompile);
0101     connect(CompilerExplorerSvc::instance(), &CompilerExplorerSvc::asmResult, this, &CEWidget::processAndShowAsm);
0102 
0103     connect(this, &CEWidget::lineHovered, m_model, &AsmViewModel::highlightLinkedAsm);
0104     connect(m_asmView, &AsmView::scrollToLineRequested, this, [this](int line) {
0105         m_textEditor->setCursorPosition({line, 0});
0106     });
0107 
0108     QString file = doc->url().toLocalFile();
0109     QString compilecmds = CompileDBReader::locateCompileCommands(m_mainWindow, file);
0110     QString args = CompileDBReader::filteredArgsForFile(compilecmds, file);
0111     m_lineEdit->setText(args);
0112 
0113     warnIfBadArgs(args.split(QLatin1Char(' ')));
0114 
0115     setFocusPolicy(Qt::StrongFocus);
0116 }
0117 
0118 CEWidget::~CEWidget()
0119 {
0120     removeViewAsActiveXMLGuiClient();
0121 }
0122 
0123 bool CEWidget::shouldClose()
0124 {
0125     int ret =
0126         KMessageBox::warningTwoActions(this, i18n("Do you really want to close %1?", windowTitle()), {}, KStandardGuiItem::close(), KStandardGuiItem::cancel());
0127     return ret == KMessageBox::PrimaryAction;
0128 }
0129 
0130 void CEWidget::removeViewAsActiveXMLGuiClient()
0131 {
0132     if (m_textEditor) {
0133         m_mainWindow->guiFactory()->removeClient(m_textEditor);
0134     }
0135 }
0136 
0137 bool CEWidget::eventFilter(QObject *o, QEvent *e)
0138 {
0139     // We live in a stacked widget in kateviewspace
0140     // use hide/show to figure out when we are not active
0141     if (e->type() == QEvent::Show) {
0142         if (m_textEditor) {
0143             m_mainWindow->guiFactory()->addClient(m_textEditor);
0144         }
0145         return QWidget::eventFilter(o, e);
0146     } else if (e->type() == QEvent::Hide) {
0147         removeViewAsActiveXMLGuiClient();
0148         return QWidget::eventFilter(o, e);
0149     }
0150 
0151     if (o != m_textEditor) {
0152         return QWidget::eventFilter(o, e);
0153     }
0154 
0155     if (e->type() != QEvent::HoverMove) {
0156         return QWidget::eventFilter(o, e);
0157     }
0158 
0159     auto event = static_cast<QHoverEvent *>(e);
0160     auto cursor = m_textEditor->coordinatesToCursor(event->position().toPoint());
0161     Q_EMIT lineHovered(cursor.line()); // Can be invalid, that is okay
0162     m_asmView->viewport()->update();
0163 
0164     return QWidget::eventFilter(o, e);
0165 }
0166 
0167 void CEWidget::createTopBar(QVBoxLayout *mainLayout)
0168 {
0169     QHBoxLayout *topBarLayout = new QHBoxLayout;
0170     mainLayout->addLayout(topBarLayout);
0171 
0172     topBarLayout->addWidget(m_languagesCombo);
0173     topBarLayout->addWidget(m_compilerCombo);
0174     topBarLayout->addWidget(m_optsCombo);
0175     topBarLayout->addWidget(m_lineEdit);
0176     topBarLayout->addWidget(m_compileButton);
0177 
0178     auto svc = CompilerExplorerSvc::instance();
0179 
0180     connect(svc, &CompilerExplorerSvc::languages, this, &CEWidget::setAvailableLanguages);
0181     svc->sendRequest(CompilerExplorer::Languages);
0182 
0183     connect(svc, &CompilerExplorerSvc::compilers, this, &CEWidget::setAvailableCompilers);
0184     svc->sendRequest(CompilerExplorer::Compilers);
0185 
0186     m_compileButton->setText(i18n("Compile"));
0187 
0188     initOptionsComboBox();
0189 }
0190 
0191 void CEWidget::setAvailableLanguages(const QByteArray &data)
0192 {
0193     if (!doc) {
0194         return;
0195     }
0196 
0197     const QJsonArray langs = QJsonDocument::fromJson(data).array();
0198 
0199     // We use the highlightingMode to set the current active language,
0200     // but this needs some sort of mapping to CE's lang names
0201     auto currentFileLang = doc->highlightingMode();
0202     QString activeLang;
0203 
0204     m_languagesCombo->clear();
0205 
0206     for (const auto &langJV : langs) {
0207         const auto lang = langJV.toObject();
0208         const auto id = lang.value(QStringLiteral("id")).toString();
0209         const auto name = lang.value(QStringLiteral("name")).toString();
0210 
0211         if (name == currentFileLang) {
0212             activeLang = name;
0213         }
0214 
0215         m_languagesCombo->addItem(name, id);
0216     }
0217     m_languagesCombo->setCurrentText(activeLang);
0218     m_languagesCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
0219 
0220     connect(m_languagesCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
0221         QString id = m_languagesCombo->itemData(index).toString();
0222         repopulateCompilersCombo(id);
0223     });
0224 }
0225 
0226 void CEWidget::setAvailableCompilers(const QByteArray &data)
0227 {
0228     if (!doc) {
0229         return;
0230     }
0231 
0232     const QJsonArray json = QJsonDocument::fromJson(data).array();
0233 
0234     m_langToCompiler.clear();
0235 
0236     for (const auto &value : json) {
0237         const auto compilerName = value[QStringLiteral("name")].toString();
0238         const auto lang = value[QStringLiteral("lang")].toString();
0239         const auto id = value[QStringLiteral("id")];
0240 
0241         Compiler compiler{compilerName, id};
0242         m_langToCompiler.push_back({lang, compiler});
0243     }
0244 
0245     repopulateCompilersCombo(doc->highlightingMode().toLower());
0246     m_compilerCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
0247 }
0248 
0249 void CEWidget::repopulateCompilersCombo(const QString &lang)
0250 {
0251     auto currentFileLang = lang;
0252 
0253     auto compilersForLang = compilersForLanguage(currentFileLang);
0254     if (compilersForLang.empty()) {
0255         compilersForLang = m_langToCompiler;
0256     }
0257 
0258     m_compilerCombo->clear();
0259 
0260     for (const auto &[lang, compiler] : compilersForLang) {
0261         m_compilerCombo->addItem(compiler.name, compiler.id);
0262     }
0263 
0264     m_compilerCombo->setCurrentIndex(0);
0265 }
0266 
0267 std::vector<CEWidget::CompilerLangPair> CEWidget::compilersForLanguage(const QString &lang) const
0268 {
0269     std::vector<CompilerLangPair> compilersForLang;
0270     for (const auto &pair : m_langToCompiler) {
0271         if (pair.first == lang) {
0272             compilersForLang.push_back(pair);
0273         }
0274     }
0275     return compilersForLang;
0276 }
0277 
0278 void CEWidget::initOptionsComboBox()
0279 {
0280     QMenu *menu = new QMenu(this);
0281     m_optsCombo->setMenu(menu);
0282     m_optsCombo->setToolButtonStyle(Qt::ToolButtonTextOnly);
0283     m_optsCombo->setText(i18n("Options"));
0284     m_optsCombo->setPopupMode(QToolButton::InstantPopup);
0285     m_optsCombo->setArrowType(Qt::DownArrow);
0286 
0287     auto checkableAction = [this](const QString &name, CE_Options o) {
0288         QAction *action = new QAction(name, this);
0289         action->setCheckable(true);
0290         action->setChecked(readConfigForCEOption(o));
0291         action->setData((int)o);
0292 
0293         connect(action, &QAction::toggled, this, [o](bool v) {
0294             writeConfigForCEOption(o, v);
0295         });
0296 
0297         return action;
0298     };
0299 
0300     menu->addAction(checkableAction(i18n("Demangle Identifiers"), CE_Option_Demangle));
0301     menu->addAction(checkableAction(i18n("Filter Library Functions"), CE_Option_FilterUnusedLibFuncs));
0302     menu->addAction(checkableAction(i18n("Filter Unused Labels"), CE_Option_FilterLabel));
0303     menu->addAction(checkableAction(i18n("Filter Comments"), CE_Option_FilterComments));
0304     menu->addAction(checkableAction(i18n("Intel Syntax"), CE_Option_IntelAsm));
0305 
0306     menu->addAction(i18n("Change Url..."), this, [this] {
0307         KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("kate_compilerexplorer"));
0308         QString url = cg.readEntry("kate_compilerexplorer_url", QStringLiteral("http://localhost:10240"));
0309 
0310         bool ok = false;
0311         QString newUrl = QInputDialog::getText(this,
0312                                                i18n("Enter Url"),
0313                                                i18n("Enter Url to CompilerExplorer instance. For e.g., http://localhost:10240"),
0314                                                QLineEdit::Normal,
0315                                                url,
0316                                                &ok);
0317 
0318         if (ok && !newUrl.isEmpty()) {
0319             CompilerExplorerSvc::instance()->changeUrl(newUrl);
0320             cg.writeEntry("kate_compilerexplorer_url", newUrl);
0321         }
0322     });
0323 }
0324 
0325 void CEWidget::createMainViews(QVBoxLayout *mainLayout)
0326 {
0327     if (!doc) {
0328         return;
0329     }
0330 
0331     QSplitter *splitter = new QSplitter(this);
0332 
0333     m_textEditor = doc->createView(this, m_mainWindow);
0334 
0335     m_asmView->setModel(m_model);
0336 
0337     addExtraActionstoTextEditor();
0338     m_textEditor->installEventFilter(this);
0339     m_textEditor->focusProxy()->installEventFilter(this);
0340 
0341     splitter->addWidget(m_textEditor);
0342     splitter->addWidget(m_asmView);
0343     splitter->setSizes({INT_MAX, INT_MAX});
0344 
0345     mainLayout->addWidget(splitter);
0346 }
0347 
0348 QString CEWidget::currentCompiler() const
0349 {
0350     return m_compilerCombo->currentData().toString();
0351 }
0352 
0353 bool CEWidget::compilationFailed(const QJsonObject &obj)
0354 {
0355     int colWidth = m_asmView->columnWidth(AsmViewModel::Column_Text);
0356 
0357     QFontMetrics fm(m_asmView->font());
0358     int avgCharWidth = fm.averageCharWidth();
0359 
0360     int maxChars = colWidth / avgCharWidth;
0361 
0362     auto code = obj.value(QStringLiteral("code"));
0363     if (!code.isUndefined()) {
0364         int compilerReturnCode = code.toInt();
0365         if (compilerReturnCode != 0) {
0366             const auto stderror = obj.value(QStringLiteral("stderr")).toArray();
0367             std::vector<AsmRow> rows;
0368 
0369             // strip any escape sequences
0370             static const QRegularExpression re(QStringLiteral("[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]"));
0371 
0372             AsmRow r;
0373             r.text = i18n("Compiler returned: %1", compilerReturnCode);
0374             rows.push_back(r);
0375 
0376             for (const auto &err : stderror) {
0377                 QString text = err.toObject().value(QStringLiteral("text")).toString();
0378                 text.replace(re, QLatin1String(""));
0379 
0380                 QStringList lines;
0381                 lines.reserve(text.size() / maxChars);
0382                 for (int i = 0; i < text.size(); i += maxChars) {
0383                     lines << text.mid(i, maxChars);
0384                 }
0385 
0386                 for (const auto &line : std::as_const(lines)) {
0387                     AsmRow r;
0388                     r.text = line;
0389                     rows.push_back(r);
0390                 }
0391             }
0392 
0393             m_model->setDataFromCE(std::move(rows), {}, {});
0394             return true;
0395         }
0396     }
0397     return false;
0398 }
0399 
0400 void CEWidget::processAndShowAsm(const QByteArray &data)
0401 {
0402     m_model->clear();
0403 
0404     std::vector<AsmRow> rows;
0405     QHash<SourcePos, AsmViewModel::CodeGenLines> sourceToAsm;
0406 
0407     const auto json = QJsonDocument::fromJson(data);
0408 
0409     const auto mainObj = json.object();
0410 
0411     if (compilationFailed(mainObj)) {
0412         m_model->setHasError(true);
0413         return;
0414     }
0415     m_model->setHasError(false);
0416 
0417     //     printf("%s\n", json.toJson().constData());
0418     const QJsonArray assembly = mainObj.value(QStringLiteral("asm")).toArray();
0419     rows.reserve(assembly.size());
0420 
0421     int currentAsmLine = 0;
0422 
0423     // Process the assembly
0424     for (const auto &line : assembly) {
0425         AsmRow row;
0426 
0427         auto labels = line[QStringLiteral("labels")].toArray();
0428         if (!labels.empty()) {
0429             for (const auto &label : labels) {
0430                 LabelInRow l;
0431                 auto rangeJV = label.toObject().value(QStringLiteral("range"));
0432 
0433                 const auto range = rangeJV.toObject();
0434                 int startCol = range.value(QStringLiteral("startCol")).toInt() - 1;
0435                 int endCol = range.value(QStringLiteral("endCol")).toInt();
0436                 l.col = startCol;
0437                 l.len = endCol - startCol;
0438                 row.labels.push_back(l);
0439             }
0440         }
0441 
0442         const auto source = line[QStringLiteral("source")].toObject();
0443         QString file = source.value(QStringLiteral("file")).toString();
0444         int srcLine = source.value(QStringLiteral("line")).toInt();
0445         int srcCol = source.value(QStringLiteral("column")).toInt();
0446 
0447         row.source.file = file.isEmpty() ? QString() : file; // can be empty
0448         row.source.line = srcLine;
0449         row.source.col = srcCol;
0450 
0451         row.text = line[QStringLiteral("text")].toString();
0452 
0453         rows.push_back(row);
0454         sourceToAsm[row.source].push_back(currentAsmLine);
0455 
0456         currentAsmLine++;
0457     }
0458 
0459     const QJsonObject labelDefinitions = mainObj.value(QStringLiteral("labelDefinitions")).toObject();
0460     // Label => Line Number
0461     QHash<QString, int> labelDefs;
0462     auto it = labelDefinitions.constBegin();
0463     auto end = labelDefinitions.constEnd();
0464     for (; it != end; ++it) {
0465         labelDefs.insert(it.key(), it.value().toInt());
0466     }
0467 
0468     m_model->setDataFromCE(std::move(rows), std::move(sourceToAsm), std::move(labelDefs));
0469     m_asmView->resizeColumnToContents(0);
0470 }
0471 
0472 void CEWidget::doCompile()
0473 {
0474     m_model->clear();
0475 
0476     if (!doc) {
0477         return;
0478     }
0479 
0480     const QString text = doc->text();
0481     if (text.isEmpty()) {
0482         return;
0483     }
0484 
0485     const auto actions = m_optsCombo->menu()->actions();
0486 
0487     bool demangle = false;
0488     bool intel = false;
0489     bool labels = false;
0490     bool comments = false;
0491     bool libfuncs = false;
0492     for (auto action : actions) {
0493         bool isChecked = action->isChecked();
0494         if (action->data().toInt() == CE_Option_Demangle)
0495             demangle = isChecked;
0496         else if (action->data().toInt() == CE_Option_FilterComments)
0497             comments = isChecked;
0498         else if (action->data().toInt() == CE_Option_FilterLabel)
0499             labels = isChecked;
0500         else if (action->data().toInt() == CE_Option_FilterUnusedLibFuncs)
0501             libfuncs = isChecked;
0502         else if (action->data().toInt() == CE_Option_IntelAsm)
0503             intel = isChecked;
0504     }
0505 
0506     QString args2 = m_lineEdit->text().trimmed();
0507 
0508     const auto json = CompilerExplorerSvc::getCompilationOptions(text, args2, intel, demangle, labels, comments, libfuncs);
0509     const auto compiler = currentCompiler();
0510     const QString endpoint = QStringLiteral("compiler/") + compiler + QStringLiteral("/compile");
0511 
0512     CompilerExplorerSvc::instance()->compileRequest(endpoint, json.toJson());
0513 }
0514 
0515 void CEWidget::addExtraActionstoTextEditor()
0516 {
0517     Q_ASSERT(m_textEditor);
0518 
0519     auto m = m_textEditor->defaultContextMenu();
0520 
0521     QMenu *menu = new QMenu(this);
0522     menu->addAction(i18n("Reveal linked code"), this, [this] {
0523         auto line = m_textEditor->cursorPosition().line();
0524         SourcePos p{QString(), line + 1, 0};
0525         AsmViewModel::CodeGenLines asmLines = m_model->asmForSourcePos(p);
0526         //         qDebug() << "Linked code for: " << line;
0527         if (!asmLines.empty()) {
0528             auto index = m_model->index(asmLines.front(), 0);
0529             m_asmView->scrollTo(index, QAbstractItemView::PositionAtCenter);
0530 
0531             // Highlight the linked lines
0532             Q_EMIT lineHovered(line);
0533             m_asmView->viewport()->update();
0534             //             qDebug() << "Scrolling to: " << index.data() << asmLines.size() << asmLines.front();
0535         }
0536     });
0537     menu->addActions(m->actions());
0538     m_textEditor->setContextMenu(menu);
0539 }
0540 
0541 void CEWidget::sendMessage(const QString &plainText, bool warn)
0542 {
0543     // use generic output view
0544     Utils::showMessage(plainText, {}, i18n("CompilerExplorer"), warn ? MessageType::Error : MessageType::Info);
0545 }
0546 
0547 void CEWidget::warnIfBadArgs(const QStringList &args)
0548 {
0549     QStringList warnableArgs = {QStringLiteral("flto"), QStringLiteral("fsanitize")};
0550     QStringList found;
0551     for (const auto &a : args) {
0552         for (const auto &w : warnableArgs) {
0553             if (a.contains(w)) {
0554                 warnableArgs.removeOne(w);
0555                 found.append(w);
0556             }
0557         }
0558     }
0559 
0560     QString msg = i18n("'%1' compiler flags were found. Output may not be useful.", found.join(QLatin1String(", ")));
0561     sendMessage(msg, true);
0562 }
0563 
0564 #include "moc_ce_widget.cpp"