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

0001 /* plugin_katebuild.c                    Kate Plugin
0002 **
0003 ** SPDX-FileCopyrightText: 2013 Alexander Neundorf <neundorf@kde.org>
0004 ** SPDX-FileCopyrightText: 2006-2015 Kåre Särs <kare.sars@iki.fi>
0005 ** SPDX-FileCopyrightText: 2011 Ian Wakeling <ian.wakeling@ntlworld.com>
0006 **
0007 ** This code is mostly a modification of the GPL'ed Make plugin
0008 ** by Adriaan de Groot.
0009 */
0010 
0011 /*
0012 ** SPDX-License-Identifier: GPL-2.0-or-later
0013 **
0014 ** This program is distributed in the hope that it will be useful,
0015 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
0016 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0017 ** GNU General Public License for more details.
0018 **
0019 ** You should have received a copy of the GNU General Public License
0020 ** along with this program in a file called COPYING; if not, write to
0021 ** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
0022 ** MA 02110-1301, USA.
0023 */
0024 
0025 #include "plugin_katebuild.h"
0026 
0027 #include "AppOutput.h"
0028 #include "buildconfig.h"
0029 #include "hostprocess.h"
0030 
0031 #include <cassert>
0032 
0033 #include <QApplication>
0034 #include <QCompleter>
0035 #include <QDir>
0036 #include <QFileDialog>
0037 #include <QFileInfo>
0038 #include <QFontDatabase>
0039 #include <QIcon>
0040 #include <QJsonArray>
0041 #include <QJsonDocument>
0042 #include <QJsonObject>
0043 #include <QKeyEvent>
0044 #include <QRegularExpressionMatch>
0045 #include <QScrollBar>
0046 #include <QString>
0047 #include <QTimer>
0048 
0049 #include <QAction>
0050 
0051 #include <KActionCollection>
0052 #include <KTextEditor/Application>
0053 #include <KTextEditor/Editor>
0054 
0055 #include <KAboutData>
0056 #include <KColorScheme>
0057 #include <KLocalizedString>
0058 #include <KMessageBox>
0059 #include <KPluginFactory>
0060 #include <KXMLGUIFactory>
0061 
0062 #include <kterminallauncherjob.h>
0063 #include <ktexteditor_utils.h>
0064 
0065 #include <kde_terminal_interface.h>
0066 #include <kparts/part.h>
0067 
0068 K_PLUGIN_FACTORY_WITH_JSON(KateBuildPluginFactory, "katebuildplugin.json", registerPlugin<KateBuildPlugin>();)
0069 
0070 static const QString DefConfigCmd = QStringLiteral("cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_EXPORT_COMPILE_COMMANDS=1 ../");
0071 static const QString DefConfClean;
0072 static const QString DefTargetName = QStringLiteral("build");
0073 static const QString DefBuildCmd = QStringLiteral("make");
0074 static const QString DefCleanCmd = QStringLiteral("make clean");
0075 static const QString DiagnosticsPrefix = QStringLiteral("katebuild");
0076 
0077 #ifdef Q_OS_WIN
0078 /******************************************************************/
0079 static QString caseFixed(const QString &path)
0080 {
0081     QStringList paths = path.split(QLatin1Char('/'));
0082     if (paths.isEmpty()) {
0083         return path;
0084     }
0085 
0086     QString result = paths[0].toUpper() + QLatin1Char('/');
0087     for (int i = 1; i < paths.count(); ++i) {
0088         QDir curDir(result);
0089         const QStringList items = curDir.entryList();
0090         int j;
0091         for (j = 0; j < items.size(); ++j) {
0092             if (items[j].compare(paths[i], Qt::CaseInsensitive) == 0) {
0093                 result += items[j];
0094                 if (i < paths.count() - 1) {
0095                     result += QLatin1Char('/');
0096                 }
0097                 break;
0098             }
0099         }
0100         if (j == items.size()) {
0101             return path;
0102         }
0103     }
0104     return result;
0105 }
0106 
0107 // clang-format off
0108 #include <windows.h>
0109 #include <Tlhelp32.h>
0110 // clang-format on
0111 
0112 static void KillProcessTree(DWORD myprocID)
0113 {
0114     if (myprocID == 0) {
0115         return;
0116     }
0117     PROCESSENTRY32 procEntry;
0118     memset(&procEntry, 0, sizeof(PROCESSENTRY32));
0119     procEntry.dwSize = sizeof(PROCESSENTRY32);
0120 
0121     HANDLE hSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
0122 
0123     if (::Process32First(hSnap, &procEntry)) {
0124         do {
0125             if (procEntry.th32ParentProcessID == myprocID) {
0126                 KillProcessTree(procEntry.th32ProcessID);
0127             }
0128         } while (::Process32Next(hSnap, &procEntry));
0129     }
0130 
0131     // kill the main process
0132     HANDLE hProc = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, myprocID);
0133 
0134     if (hProc) {
0135         ::TerminateProcess(hProc, 1);
0136         ::CloseHandle(hProc);
0137     }
0138 }
0139 
0140 static void terminateProcess(KProcess &proc)
0141 {
0142     KillProcessTree(proc.processId());
0143 }
0144 
0145 #else
0146 static QString caseFixed(const QString &path)
0147 {
0148     return path;
0149 }
0150 
0151 static void terminateProcess(KProcess &proc)
0152 {
0153     proc.terminate();
0154 }
0155 #endif
0156 
0157 struct ItemData {
0158     // ensure destruction, but not inadvertently so by a variant value copy
0159     std::shared_ptr<KTextEditor::MovingCursor> cursor;
0160 };
0161 
0162 Q_DECLARE_METATYPE(ItemData)
0163 
0164 /******************************************************************/
0165 KateBuildPlugin::KateBuildPlugin(QObject *parent, const VariantList &)
0166     : KTextEditor::Plugin(parent)
0167 {
0168 }
0169 
0170 /******************************************************************/
0171 QObject *KateBuildPlugin::createView(KTextEditor::MainWindow *mainWindow)
0172 {
0173     return new KateBuildView(this, mainWindow);
0174 }
0175 
0176 /******************************************************************/
0177 int KateBuildPlugin::configPages() const
0178 {
0179     return 1;
0180 }
0181 
0182 /******************************************************************/
0183 KTextEditor::ConfigPage *KateBuildPlugin::configPage(int number, QWidget *parent)
0184 {
0185     if (number != 0) {
0186         return nullptr;
0187     }
0188 
0189     KateBuildConfigPage *configPage = new KateBuildConfigPage(parent);
0190     connect(configPage, &KateBuildConfigPage::configChanged, this, &KateBuildPlugin::configChanged);
0191     return configPage;
0192 }
0193 
0194 /******************************************************************/
0195 KateBuildView::KateBuildView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mw)
0196     : QObject(mw)
0197     , m_win(mw)
0198     , m_buildWidget(nullptr)
0199     , m_proc(this)
0200     , m_stdOut()
0201     , m_stdErr()
0202     , m_buildCancelled(false)
0203     // NOTE this will not allow spaces in file names.
0204     // e.g. from gcc: "main.cpp:14: error: cannot convert ‘std::string’ to ‘int’ in return"
0205     // e.g. from gcc: "main.cpp:14:8: error: cannot convert ‘std::string’ to ‘int’ in return"
0206     // e.g. from icpc: "main.cpp(14): error: no suitable conversion function from "std::string" to "int" exists"
0207     // e.g. from clang: ""main.cpp(14,8): fatal error: 'boost/scoped_array.hpp' file not found"
0208     , m_filenameDetector(QStringLiteral("(?<filename>(?:[a-np-zA-Z]:[\\\\/])?[^\\s:(]+)[:(](?<line>\\d+)[,:]?(?<column>\\d+)?[):]* (?<message>.*)"))
0209     , m_newDirDetector(QStringLiteral("make\\[.+\\]: .+ '(.*)'"))
0210     , m_diagnosticsProvider(mw, this)
0211 {
0212     KXMLGUIClient::setComponentName(QStringLiteral("katebuild"), i18n("Build"));
0213     setXMLFile(QStringLiteral("ui.rc"));
0214 
0215     m_toolView = mw->createToolView(plugin,
0216                                     QStringLiteral("kate_plugin_katebuildplugin"),
0217                                     KTextEditor::MainWindow::Bottom,
0218                                     QIcon::fromTheme(QStringLiteral("run-build-clean")),
0219                                     i18n("Build"));
0220 
0221     QAction *a = actionCollection()->addAction(QStringLiteral("select_target"));
0222     a->setText(i18n("Select Target..."));
0223     a->setIcon(QIcon::fromTheme(QStringLiteral("select")));
0224     connect(a, &QAction::triggered, this, &KateBuildView::slotSelectTarget);
0225 
0226     a = actionCollection()->addAction(QStringLiteral("build_selected_target"));
0227     a->setText(i18n("Build Selected Target"));
0228     a->setIcon(QIcon::fromTheme(QStringLiteral("run-build")));
0229     connect(a, &QAction::triggered, this, &KateBuildView::slotBuildSelectedTarget);
0230 
0231     a = actionCollection()->addAction(QStringLiteral("build_and_run_selected_target"));
0232     a->setText(i18n("Build and Run Selected Target"));
0233     a->setIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
0234     connect(a, &QAction::triggered, this, &KateBuildView::slotBuildAndRunSelectedTarget);
0235 
0236     a = actionCollection()->addAction(QStringLiteral("stop"));
0237     a->setText(i18n("Stop"));
0238     a->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
0239     connect(a, &QAction::triggered, this, &KateBuildView::slotStop);
0240 
0241     a = actionCollection()->addAction(QStringLiteral("focus_build_tab_left"));
0242     a->setText(i18nc("Left is also left in RTL mode", "Focus Next Tab to the Left"));
0243     a->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
0244     connect(a, &QAction::triggered, this, [this]() {
0245         int index = m_buildUi.u_tabWidget->currentIndex();
0246         if (!m_toolView->isVisible()) {
0247             m_win->showToolView(m_toolView);
0248         } else {
0249             index += qApp->layoutDirection() == Qt::RightToLeft ? 1 : -1;
0250             if (index >= m_buildUi.u_tabWidget->count()) {
0251                 index = 0;
0252             }
0253             if (index < 0) {
0254                 index = m_buildUi.u_tabWidget->count() - 1;
0255             }
0256         }
0257         m_buildUi.u_tabWidget->setCurrentIndex(index);
0258         m_buildUi.u_tabWidget->widget(index)->setFocus();
0259     });
0260 
0261     a = actionCollection()->addAction(QStringLiteral("focus_build_tab_right"));
0262     a->setText(i18nc("Right is right also in RTL mode", "Focus Next Tab to the Right"));
0263     a->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
0264     connect(a, &QAction::triggered, this, [this]() {
0265         int index = m_buildUi.u_tabWidget->currentIndex();
0266         if (!m_toolView->isVisible()) {
0267             m_win->showToolView(m_toolView);
0268         } else {
0269             index += qApp->layoutDirection() == Qt::RightToLeft ? -1 : 1;
0270             if (index >= m_buildUi.u_tabWidget->count()) {
0271                 index = 0;
0272             }
0273             if (index < 0) {
0274                 index = m_buildUi.u_tabWidget->count() - 1;
0275             }
0276         }
0277         m_buildUi.u_tabWidget->setCurrentIndex(index);
0278         m_buildUi.u_tabWidget->widget(index)->setFocus();
0279     });
0280 
0281     m_buildWidget = new QWidget(m_toolView);
0282     m_buildUi.setupUi(m_buildWidget);
0283     m_targetsUi = new TargetsUi(this, m_buildUi.u_tabWidget);
0284     m_buildUi.u_tabWidget->insertTab(0, m_targetsUi, i18nc("Tab label", "Target Settings"));
0285     m_buildUi.u_tabWidget->setCurrentWidget(m_targetsUi);
0286     m_buildUi.u_tabWidget->setTabsClosable(true);
0287     m_buildUi.u_tabWidget->tabBar()->tabButton(0, QTabBar::RightSide)->hide();
0288     m_buildUi.u_tabWidget->tabBar()->tabButton(1, QTabBar::RightSide)->hide();
0289     connect(m_buildUi.u_tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) {
0290         // FIXME check if the process is still running
0291         m_buildUi.u_tabWidget->widget(index)->deleteLater();
0292     });
0293 
0294     connect(m_buildUi.u_tabWidget->tabBar(), &QTabBar::tabBarClicked, this, [this](int index) {
0295         if (QWidget *tabWidget = m_buildUi.u_tabWidget->widget(index)) {
0296             tabWidget->setFocus();
0297         }
0298     });
0299 
0300     m_buildWidget->installEventFilter(this);
0301 
0302     m_buildUi.buildAgainButton->setVisible(true);
0303     m_buildUi.cancelBuildButton->setVisible(true);
0304     m_buildUi.buildStatusLabel->setVisible(true);
0305     m_buildUi.cancelBuildButton->setEnabled(false);
0306 
0307     m_buildUi.textBrowser->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
0308     m_buildUi.textBrowser->setWordWrapMode(QTextOption::NoWrap);
0309     m_buildUi.textBrowser->setReadOnly(true);
0310     m_buildUi.textBrowser->setOpenLinks(false);
0311     connect(m_buildUi.textBrowser, &QTextBrowser::anchorClicked, this, [this](const QUrl &url) {
0312         static QRegularExpression fileRegExp(QStringLiteral("(.*):(\\d+):(\\d+)"));
0313         const auto match = fileRegExp.match(url.toString(QUrl::None));
0314         if (!match.hasMatch() || !m_win) {
0315             return;
0316         };
0317 
0318         QString filePath = match.captured(1);
0319         if (!QFile::exists(filePath)) {
0320             filePath = caseFixed(filePath);
0321             if (!QFile::exists(filePath)) {
0322                 return;
0323             }
0324         }
0325 
0326         QUrl fileUrl = QUrl::fromLocalFile(filePath);
0327         m_win->openUrl(fileUrl);
0328         if (!m_win->activeView()) {
0329             return;
0330         }
0331         int lineNr = match.captured(2).toInt();
0332         int column = match.captured(3).toInt();
0333         m_win->activeView()->setCursorPosition(KTextEditor::Cursor(lineNr - 1, column - 1));
0334         m_win->activeView()->setFocus();
0335     });
0336     m_outputTimer.setSingleShot(true);
0337     m_outputTimer.setInterval(100);
0338     connect(&m_outputTimer, &QTimer::timeout, this, &KateBuildView::updateTextBrowser);
0339 
0340     auto updateEditorColors = [this](KTextEditor::Editor *e) {
0341         if (!e)
0342             return;
0343         auto theme = e->theme();
0344         auto bg = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::BackgroundColor));
0345         auto fg = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::TextStyle::Normal));
0346         auto sel = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::TextSelection));
0347         auto errBg = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::MarkError));
0348         auto warnBg = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::MarkWarning));
0349         auto noteBg = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::MarkBookmark));
0350         errBg.setAlpha(30);
0351         warnBg.setAlpha(30);
0352         noteBg.setAlpha(30);
0353         auto pal = m_buildUi.textBrowser->palette();
0354         pal.setColor(QPalette::Base, bg);
0355         pal.setColor(QPalette::Text, fg);
0356         pal.setColor(QPalette::Highlight, sel);
0357         pal.setColor(QPalette::HighlightedText, fg);
0358         m_buildUi.textBrowser->setPalette(pal);
0359         m_buildUi.textBrowser->document()->setDefaultStyleSheet(QStringLiteral("a{text-decoration:none;}"
0360                                                                                "a:link{color:%1;}\n"
0361                                                                                ".err-text {color:%1; background-color: %2;}"
0362                                                                                ".warn-text {color:%1; background-color: %3;}"
0363                                                                                ".note-text {color:%1; background-color: %4;}")
0364                                                                     .arg(fg.name(QColor::HexArgb))
0365                                                                     .arg(errBg.name(QColor::HexArgb))
0366                                                                     .arg(warnBg.name(QColor::HexArgb))
0367                                                                     .arg(noteBg.name(QColor::HexArgb)));
0368         updateTextBrowser();
0369     };
0370     connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateEditorColors);
0371 
0372     connect(m_buildUi.buildAgainButton, &QPushButton::clicked, this, &KateBuildView::slotBuildPreviousTarget);
0373     connect(m_buildUi.cancelBuildButton, &QPushButton::clicked, this, &KateBuildView::slotStop);
0374 
0375     connect(m_targetsUi->newTarget, &QToolButton::clicked, this, &KateBuildView::targetSetNew);
0376     connect(m_targetsUi->copyTarget, &QToolButton::clicked, this, &KateBuildView::targetOrSetCopy);
0377     connect(m_targetsUi->deleteTarget, &QToolButton::clicked, this, &KateBuildView::targetDelete);
0378 
0379     connect(m_targetsUi->addButton, &QToolButton::clicked, this, &KateBuildView::slotAddTargetClicked);
0380     connect(m_targetsUi->buildButton, &QToolButton::clicked, this, &KateBuildView::slotBuildSelectedTarget);
0381     connect(m_targetsUi->runButton, &QToolButton::clicked, this, &KateBuildView::slotBuildAndRunSelectedTarget);
0382     connect(m_targetsUi, &TargetsUi::enterPressed, this, &KateBuildView::slotBuildAndRunSelectedTarget);
0383 
0384     m_proc.setOutputChannelMode(KProcess::SeparateChannels);
0385     connect(&m_proc, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &KateBuildView::slotProcExited);
0386     connect(&m_proc, &KProcess::readyReadStandardError, this, &KateBuildView::slotReadReadyStdErr);
0387     connect(&m_proc, &KProcess::readyReadStandardOutput, this, &KateBuildView::slotReadReadyStdOut);
0388 
0389     connect(m_win, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KateBuildView::handleEsc);
0390 
0391     m_toolView->installEventFilter(this);
0392 
0393     m_win->guiFactory()->addClient(this);
0394 
0395     // watch for project plugin view creation/deletion
0396     connect(m_win, &KTextEditor::MainWindow::pluginViewCreated, this, &KateBuildView::slotPluginViewCreated);
0397     connect(m_win, &KTextEditor::MainWindow::pluginViewDeleted, this, &KateBuildView::slotPluginViewDeleted);
0398 
0399     // Connect signals from project plugin to our slots
0400     m_projectPluginView = m_win->pluginView(QStringLiteral("kateprojectplugin"));
0401     slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView);
0402 
0403     m_diagnosticsProvider.name = i18n("Build Information");
0404     m_diagnosticsProvider.setPersistentDiagnostics(true);
0405 
0406     connect(&m_targetsUi->targetsModel, &TargetModel::projectTargetChanged, this, &KateBuildView::saveProjectTargets);
0407     connect(m_targetsUi->moveTargetUp, &QToolButton::clicked, this, [this]() {
0408         const QPersistentModelIndex &currentIndex = m_targetsUi->proxyModel.mapToSource(m_targetsUi->targetsView->currentIndex());
0409         if (currentIndex.isValid()) {
0410             m_targetsUi->targetsModel.moveRowUp(currentIndex);
0411             if (currentIndex.data(TargetModel::IsProjectTargetRole).toBool() && currentIndex.data(TargetModel::RowTypeRole).toInt() != TargetModel::RootRow) {
0412                 saveProjectTargets();
0413             }
0414         }
0415         m_targetsUi->targetsView->scrollTo(m_targetsUi->targetsView->currentIndex());
0416     });
0417     connect(m_targetsUi->moveTargetDown, &QToolButton::clicked, this, [this]() {
0418         const QPersistentModelIndex &currentIndex = m_targetsUi->proxyModel.mapToSource(m_targetsUi->targetsView->currentIndex());
0419         if (currentIndex.isValid()) {
0420             m_targetsUi->targetsModel.moveRowDown(currentIndex);
0421             if (currentIndex.data(TargetModel::IsProjectTargetRole).toBool() && currentIndex.data(TargetModel::RowTypeRole).toInt() != TargetModel::RootRow) {
0422                 saveProjectTargets();
0423             }
0424         }
0425         m_targetsUi->targetsView->scrollTo(m_targetsUi->targetsView->currentIndex());
0426     });
0427 
0428     KateBuildPlugin *bPlugin = qobject_cast<KateBuildPlugin *>(plugin);
0429     if (bPlugin) {
0430         connect(bPlugin, &KateBuildPlugin::configChanged, this, &KateBuildView::readConfig);
0431     }
0432     readConfig();
0433 }
0434 
0435 /******************************************************************/
0436 KateBuildView::~KateBuildView()
0437 {
0438     if (m_proc.state() != QProcess::NotRunning) {
0439         terminateProcess(m_proc);
0440         m_proc.waitForFinished();
0441     }
0442     clearDiagnostics();
0443     m_win->guiFactory()->removeClient(this);
0444     delete m_toolView;
0445 }
0446 
0447 /******************************************************************/
0448 void KateBuildView::readSessionConfig(const KConfigGroup &cg)
0449 {
0450     int numTargets = cg.readEntry(QStringLiteral("NumTargets"), 0);
0451     m_projectTargetsetRow = cg.readEntry("ProjectTargetSetRow", 0);
0452     m_targetsUi->targetsModel.clear(m_projectTargetsetRow > 0);
0453 
0454     QModelIndex setIndex = m_targetsUi->targetsModel.sessionRootIndex();
0455 
0456     for (int i = 0; i < numTargets; i++) {
0457         QStringList targetNames = cg.readEntry(QStringLiteral("%1 Target Names").arg(i), QStringList());
0458         QString targetSetName = cg.readEntry(QStringLiteral("%1 Target").arg(i), QString());
0459         QString buildDir = cg.readEntry(QStringLiteral("%1 BuildPath").arg(i), QString());
0460 
0461         setIndex = m_targetsUi->targetsModel.insertTargetSetAfter(setIndex, targetSetName, buildDir);
0462 
0463         // Keep a bit of backwards compatibility by ensuring that the "default" target is the first in the list
0464         QString defCmd = cg.readEntry(QStringLiteral("%1 Target Default").arg(i), QString());
0465         int defIndex = targetNames.indexOf(defCmd);
0466         if (defIndex > 0) {
0467             targetNames.move(defIndex, 0);
0468         }
0469         QModelIndex cmdIndex = setIndex;
0470         for (int tn = 0; tn < targetNames.size(); ++tn) {
0471             const QString &targetName = targetNames.at(tn);
0472             const QString &buildCmd = cg.readEntry(QStringLiteral("%1 BuildCmd %2").arg(i).arg(targetName));
0473             const QString &runCmd = cg.readEntry(QStringLiteral("%1 RunCmd %2").arg(i).arg(targetName));
0474             m_targetsUi->targetsModel.addCommandAfter(cmdIndex, targetName, buildCmd, runCmd);
0475         }
0476     }
0477 
0478     // Add project targets, if any
0479     addProjectTarget();
0480 
0481     m_targetsUi->targetsView->expandAll();
0482 
0483     // pre-select the last active target or the first target of the first set
0484     int prevTargetSetRow = cg.readEntry(QStringLiteral("Active Target Index"), 0);
0485     int prevCmdRow = cg.readEntry(QStringLiteral("Active Target Command"), 0);
0486     QModelIndex rootIndex = m_targetsUi->targetsModel.index(prevTargetSetRow);
0487     QModelIndex cmdIndex = m_targetsUi->targetsModel.index(prevCmdRow, 0, rootIndex);
0488     cmdIndex = m_targetsUi->proxyModel.mapFromSource(cmdIndex);
0489     m_targetsUi->targetsView->setCurrentIndex(cmdIndex);
0490 
0491     m_targetsUi->updateTargetsButtonStates();
0492 }
0493 
0494 /******************************************************************/
0495 void KateBuildView::writeSessionConfig(KConfigGroup &cg)
0496 {
0497     // Save the active target
0498     QModelIndex activeIndex = m_targetsUi->targetsView->currentIndex();
0499     activeIndex = m_targetsUi->proxyModel.mapToSource(activeIndex);
0500     if (activeIndex.isValid()) {
0501         if (activeIndex.parent().isValid()) {
0502             cg.writeEntry(QStringLiteral("Active Target Index"), activeIndex.parent().row());
0503             cg.writeEntry(QStringLiteral("Active Target Command"), activeIndex.row());
0504         } else {
0505             cg.writeEntry(QStringLiteral("Active Target Index"), activeIndex.row());
0506             cg.writeEntry(QStringLiteral("Active Target Command"), 0);
0507         }
0508     }
0509 
0510     const QList<TargetModel::TargetSet> targets = m_targetsUi->targetsModel.sessionTargetSets();
0511 
0512     // Don't save project target-set, but save the row index
0513     QModelIndex projRootIndex = m_targetsUi->targetsModel.projectRootIndex();
0514     m_projectTargetsetRow = projRootIndex.row();
0515     cg.writeEntry("ProjectTargetSetRow", m_projectTargetsetRow);
0516     cg.writeEntry("NumTargets", targets.size());
0517 
0518     for (int i = 0; i < targets.size(); i++) {
0519         cg.writeEntry(QStringLiteral("%1 Target").arg(i), targets[i].name);
0520         cg.writeEntry(QStringLiteral("%1 BuildPath").arg(i), targets[i].workDir);
0521         QStringList cmdNames;
0522 
0523         for (int j = 0; j < targets[i].commands.count(); j++) {
0524             const QString &cmdName = targets[i].commands[j].name;
0525             const QString &buildCmd = targets[i].commands[j].buildCmd;
0526             const QString &runCmd = targets[i].commands[j].runCmd;
0527             cmdNames << cmdName;
0528             cg.writeEntry(QStringLiteral("%1 BuildCmd %2").arg(i).arg(cmdName), buildCmd);
0529             cg.writeEntry(QStringLiteral("%1 RunCmd %2").arg(i).arg(cmdName), runCmd);
0530         }
0531         cg.writeEntry(QStringLiteral("%1 Target Names").arg(i), cmdNames);
0532     }
0533 }
0534 
0535 /******************************************************************/
0536 void KateBuildView::readConfig()
0537 {
0538     KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("BuildConfig"));
0539     m_addDiagnostics = config.readEntry(QStringLiteral("UseDiagnosticsOutput"), true);
0540     m_autoSwitchToOutput = config.readEntry(QStringLiteral("AutoSwitchToOutput"), true);
0541 }
0542 
0543 /******************************************************************/
0544 static Diagnostic createDiagnostic(int line, int column, const QString &message, const DiagnosticSeverity &severity)
0545 {
0546     Diagnostic d;
0547     d.message = message;
0548     d.source = DiagnosticsPrefix;
0549     d.severity = severity;
0550     d.range = KTextEditor::Range(KTextEditor::Cursor(line - 1, column - 1), 0);
0551     return d;
0552 }
0553 
0554 /******************************************************************/
0555 void KateBuildView::addError(const KateBuildView::OutputLine &err)
0556 {
0557     // Get filediagnostic by filename or create new if there is none
0558     auto uri = QUrl::fromLocalFile(err.file);
0559     if (!uri.isValid()) {
0560         return;
0561     }
0562     DiagnosticSeverity severity = DiagnosticSeverity::Unknown;
0563     if (err.category == Category::Error) {
0564         m_numErrors++;
0565         severity = DiagnosticSeverity::Error;
0566     } else if (err.category == Category::Warning) {
0567         m_numWarnings++;
0568         severity = DiagnosticSeverity::Warning;
0569     } else if (err.category == Category::Info) {
0570         m_numNotes++;
0571         severity = DiagnosticSeverity::Information;
0572     }
0573 
0574     if (!m_addDiagnostics) {
0575         return;
0576     }
0577 
0578     // NOTE: Limit the number of items in the diagnostics view to 200 items.
0579     // Adding more items risks making the build slow. (standard item models are slow)
0580     if ((m_numErrors + m_numWarnings + m_numNotes) > 200) {
0581         return;
0582     }
0583     updateDiagnostics(createDiagnostic(err.lineNr, err.column, err.message, severity), uri);
0584 }
0585 
0586 /******************************************************************/
0587 void KateBuildView::updateDiagnostics(Diagnostic diagnostic, const QUrl uri)
0588 {
0589     FileDiagnostics fd;
0590     fd.uri = uri;
0591     fd.diagnostics.append(diagnostic);
0592     Q_EMIT m_diagnosticsProvider.diagnosticsAdded(fd);
0593 }
0594 
0595 /******************************************************************/
0596 void KateBuildView::clearDiagnostics()
0597 {
0598     Q_EMIT m_diagnosticsProvider.requestClearDiagnostics(&m_diagnosticsProvider);
0599 }
0600 
0601 /******************************************************************/
0602 QUrl KateBuildView::docUrl()
0603 {
0604     KTextEditor::View *kv = m_win->activeView();
0605     if (!kv) {
0606         qDebug() << "no KTextEditor::View";
0607         return QUrl();
0608     }
0609 
0610     if (kv->document()->isModified()) {
0611         kv->document()->save();
0612     }
0613     return kv->document()->url();
0614 }
0615 
0616 /******************************************************************/
0617 bool KateBuildView::checkLocal(const QUrl &dir)
0618 {
0619     if (dir.path().isEmpty()) {
0620         KMessageBox::error(nullptr, i18n("There is no file or directory specified for building."));
0621         return false;
0622     } else if (!dir.isLocalFile()) {
0623         KMessageBox::error(nullptr,
0624                            i18n("The file \"%1\" is not a local file. "
0625                                 "Non-local files cannot be compiled.",
0626                                 dir.path()));
0627         return false;
0628     }
0629     return true;
0630 }
0631 
0632 /******************************************************************/
0633 void KateBuildView::clearBuildResults()
0634 {
0635     m_buildUi.textBrowser->clear();
0636     m_stdOut.clear();
0637     m_stdErr.clear();
0638     m_htmlOutput = QStringLiteral("<pre>");
0639     m_scrollStopPos = -1;
0640     m_numOutputLines = 0;
0641     m_numErrors = 0;
0642     m_numWarnings = 0;
0643     m_numNotes = 0;
0644     m_makeDirStack.clear();
0645     clearDiagnostics();
0646 }
0647 
0648 /******************************************************************/
0649 bool KateBuildView::startProcess(const QString &dir, const QString &command)
0650 {
0651     if (m_proc.state() != QProcess::NotRunning) {
0652         return false;
0653     }
0654 
0655     // clear previous runs
0656     clearBuildResults();
0657 
0658     if (m_autoSwitchToOutput) {
0659         // activate the output tab
0660         m_buildUi.u_tabWidget->setCurrentIndex(1);
0661         m_win->showToolView(m_toolView);
0662     }
0663 
0664     m_buildUi.u_tabWidget->setTabIcon(1, QIcon::fromTheme(QStringLiteral("system-run")));
0665 
0666     QFont font = Utils::editorFont();
0667     m_buildUi.textBrowser->setFont(font);
0668 
0669     // set working directory
0670     m_makeDir = dir;
0671     m_makeDirStack.push(m_makeDir);
0672 
0673     if (!QFile::exists(m_makeDir)) {
0674         KMessageBox::error(nullptr, i18n("Cannot run command: %1\nWork path does not exist: %2", command, m_makeDir));
0675         return false;
0676     }
0677 
0678     // chdir used by QProcess will resolve symbolic links.
0679     // Define PWD so that shell scripts can get a path with symbolic links intact
0680     auto env = QProcessEnvironment::systemEnvironment();
0681     env.insert(QStringLiteral("PWD"), QDir(m_makeDir).absolutePath());
0682     m_proc.setProcessEnvironment(env);
0683     m_proc.setWorkingDirectory(m_makeDir);
0684     m_proc.setShellCommand(command);
0685     startHostProcess(m_proc);
0686 
0687     if (!m_proc.waitForStarted(500)) {
0688         KMessageBox::error(nullptr, i18n("Failed to run \"%1\". exitStatus = %2", command, m_proc.exitStatus()));
0689         return false;
0690     }
0691 
0692     m_buildUi.cancelBuildButton->setEnabled(true);
0693     m_buildUi.buildAgainButton->setEnabled(false);
0694     m_targetsUi->setCursor(Qt::BusyCursor);
0695     return true;
0696 }
0697 
0698 /******************************************************************/
0699 bool KateBuildView::slotStop()
0700 {
0701     if (m_proc.state() != QProcess::NotRunning) {
0702         m_buildCancelled = true;
0703         QString msg = i18n("Building <b>%1</b> cancelled", m_currentlyBuildingTarget);
0704         m_buildUi.buildStatusLabel->setText(msg);
0705         terminateProcess(m_proc);
0706         return true;
0707     }
0708     return false;
0709 }
0710 
0711 /******************************************************************/
0712 void KateBuildView::slotBuildSelectedTarget()
0713 {
0714     QModelIndex currentIndex = m_targetsUi->targetsView->currentIndex();
0715     if (!currentIndex.isValid() || (m_firstBuild && !m_targetsUi->targetsView->isVisible())) {
0716         slotSelectTarget();
0717         return;
0718     }
0719     m_firstBuild = false;
0720 
0721     if (!currentIndex.parent().isValid()) {
0722         // This is a root item, try to build the first command
0723         currentIndex = m_targetsUi->targetsView->model()->index(0, 0, currentIndex.siblingAtColumn(0));
0724         if (currentIndex.isValid()) {
0725             m_targetsUi->targetsView->setCurrentIndex(currentIndex);
0726         } else {
0727             slotSelectTarget();
0728             return;
0729         }
0730     }
0731     buildCurrentTarget();
0732 }
0733 
0734 /******************************************************************/
0735 void KateBuildView::slotBuildAndRunSelectedTarget()
0736 {
0737     QModelIndex currentIndex = m_targetsUi->targetsView->currentIndex();
0738     if (!currentIndex.isValid() || (m_firstBuild && !m_targetsUi->targetsView->isVisible())) {
0739         slotSelectTarget();
0740         return;
0741     }
0742     m_firstBuild = false;
0743 
0744     if (!currentIndex.parent().isValid()) {
0745         // This is a root item, try to build the first command
0746         currentIndex = m_targetsUi->targetsView->model()->index(0, 0, currentIndex.siblingAtColumn(0));
0747         if (currentIndex.isValid()) {
0748             m_targetsUi->targetsView->setCurrentIndex(currentIndex);
0749         } else {
0750             slotSelectTarget();
0751             return;
0752         }
0753     }
0754 
0755     m_runAfterBuild = true;
0756     buildCurrentTarget();
0757 }
0758 
0759 /******************************************************************/
0760 void KateBuildView::slotBuildPreviousTarget()
0761 {
0762     if (!m_previousIndex.isValid()) {
0763         slotSelectTarget();
0764     } else {
0765         m_targetsUi->targetsView->setCurrentIndex(m_previousIndex);
0766         buildCurrentTarget();
0767     }
0768 }
0769 
0770 /******************************************************************/
0771 void KateBuildView::slotSelectTarget()
0772 {
0773     m_buildUi.u_tabWidget->setCurrentIndex(0);
0774     m_win->showToolView(m_toolView);
0775     QPersistentModelIndex selected = m_targetsUi->targetsView->currentIndex();
0776     m_targetsUi->targetFilterEdit->setText(QString());
0777     m_targetsUi->targetFilterEdit->setFocus();
0778 
0779     // Flash the target selection line-edit to show that something happened
0780     // and where your focus went/should go
0781     QPalette palette = m_targetsUi->targetFilterEdit->palette();
0782     KColorScheme::adjustBackground(palette, KColorScheme::ActiveBackground);
0783     m_targetsUi->targetFilterEdit->setPalette(palette);
0784     QTimer::singleShot(500, this, [this]() {
0785         m_targetsUi->targetFilterEdit->setPalette(QPalette());
0786     });
0787 
0788     m_targetsUi->targetsView->expandAll();
0789     if (!selected.isValid()) {
0790         // We do not have a selected item. Select the first target of the first target-set
0791         QModelIndex root = m_targetsUi->targetsView->model()->index(0, 0, QModelIndex());
0792         if (root.isValid()) {
0793             selected = root.model()->index(0, 0, root);
0794         }
0795     }
0796     if (selected.isValid()) {
0797         m_targetsUi->targetsView->setCurrentIndex(selected);
0798         m_targetsUi->targetsView->scrollTo(selected);
0799     }
0800 }
0801 
0802 /******************************************************************/
0803 bool KateBuildView::buildCurrentTarget()
0804 {
0805     const QFileInfo docFInfo(docUrl().toLocalFile()); // docUrl() saves the current document
0806 
0807     QModelIndex ind = m_targetsUi->targetsView->currentIndex();
0808     m_previousIndex = ind;
0809     if (!ind.isValid()) {
0810         KMessageBox::error(nullptr, i18n("No target available for building."));
0811         return false;
0812     }
0813 
0814     QString buildCmd = ind.data(TargetModel::CommandRole).toString();
0815     QString cmdName = ind.data(TargetModel::CommandNameRole).toString();
0816     m_searchPaths = ind.data(TargetModel::SearchPathsRole).toStringList();
0817     QString workDir = ind.data(TargetModel::WorkDirRole).toString();
0818     QString targetSet = ind.data(TargetModel::TargetSetNameRole).toString();
0819 
0820     QString dir = workDir;
0821     if (workDir.isEmpty()) {
0822         dir = docFInfo.absolutePath();
0823         if (dir.isEmpty()) {
0824             KMessageBox::error(nullptr, i18n("There is no local file or directory specified for building."));
0825             return false;
0826         }
0827     }
0828 
0829     if (m_proc.state() != QProcess::NotRunning) {
0830         displayBuildResult(i18n("Already building..."), KTextEditor::Message::Warning);
0831         return false;
0832     }
0833 
0834     if (m_runAfterBuild && buildCmd.isEmpty()) {
0835         slotRunAfterBuild();
0836         return true;
0837     }
0838     // a single target can serve to build lots of projects with similar directory layout
0839     if (m_projectPluginView) {
0840         const QFileInfo baseDir(m_projectPluginView->property("projectBaseDir").toString());
0841         dir.replace(QStringLiteral("%B"), baseDir.absoluteFilePath());
0842         dir.replace(QStringLiteral("%b"), baseDir.baseName());
0843     }
0844 
0845     // Check if the command contains the file name or directory
0846     if (buildCmd.contains(QLatin1String("%f")) || buildCmd.contains(QLatin1String("%d")) || buildCmd.contains(QLatin1String("%n"))) {
0847         if (docFInfo.absoluteFilePath().isEmpty()) {
0848             return false;
0849         }
0850 
0851         buildCmd.replace(QStringLiteral("%n"), docFInfo.baseName());
0852         buildCmd.replace(QStringLiteral("%f"), docFInfo.absoluteFilePath());
0853         buildCmd.replace(QStringLiteral("%d"), docFInfo.absolutePath());
0854     }
0855     m_currentlyBuildingTarget = QStringLiteral("%1: %2").arg(targetSet, cmdName);
0856     m_buildCancelled = false;
0857     QString msg = i18n("Building target <b>%1</b> ...", m_currentlyBuildingTarget);
0858     m_buildUi.buildStatusLabel->setText(msg);
0859     return startProcess(dir, buildCmd);
0860 }
0861 
0862 /******************************************************************/
0863 void KateBuildView::displayBuildResult(const QString &msg, KTextEditor::Message::MessageType level)
0864 {
0865     KTextEditor::View *kv = m_win->activeView();
0866     if (!kv) {
0867         return;
0868     }
0869 
0870     delete m_infoMessage;
0871     m_infoMessage = new KTextEditor::Message(xi18nc("@info", "<title>Make Results:</title><nl/>%1", msg), level);
0872     m_infoMessage->setWordWrap(true);
0873     m_infoMessage->setPosition(KTextEditor::Message::BottomInView);
0874     m_infoMessage->setAutoHide(5000);
0875     m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
0876     m_infoMessage->setView(kv);
0877     kv->document()->postMessage(m_infoMessage);
0878 }
0879 
0880 /******************************************************************/
0881 void KateBuildView::displayMessage(const QString &msg, KTextEditor::Message::MessageType level)
0882 {
0883     KTextEditor::View *kv = m_win->activeView();
0884     if (!kv) {
0885         return;
0886     }
0887 
0888     delete m_infoMessage;
0889     m_infoMessage = new KTextEditor::Message(msg, level);
0890     m_infoMessage->setWordWrap(true);
0891     m_infoMessage->setPosition(KTextEditor::Message::BottomInView);
0892     m_infoMessage->setAutoHide(8000);
0893     m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
0894     m_infoMessage->setView(kv);
0895     kv->document()->postMessage(m_infoMessage);
0896 }
0897 
0898 /******************************************************************/
0899 void KateBuildView::slotProcExited(int exitCode, QProcess::ExitStatus)
0900 {
0901     m_targetsUi->unsetCursor();
0902     m_buildUi.u_tabWidget->setTabIcon(1, QIcon::fromTheme(QStringLiteral("format-justify-left")));
0903     m_buildUi.cancelBuildButton->setEnabled(false);
0904     m_buildUi.buildAgainButton->setEnabled(true);
0905 
0906     QString buildStatus =
0907         i18n("Build <b>%1</b> completed. %2 error(s), %3 warning(s), %4 note(s)", m_currentlyBuildingTarget, m_numErrors, m_numWarnings, m_numNotes);
0908 
0909     bool buildSuccess = true;
0910     if (m_numErrors || m_numWarnings) {
0911         QStringList msgs;
0912         if (m_numErrors) {
0913             msgs << i18np("Found one error.", "Found %1 errors.", m_numErrors);
0914             buildSuccess = false;
0915         }
0916         if (m_numWarnings) {
0917             msgs << i18np("Found one warning.", "Found %1 warnings.", m_numWarnings);
0918         }
0919         if (m_numNotes) {
0920             msgs << i18np("Found one note.", "Found %1 notes.", m_numNotes);
0921         }
0922         displayBuildResult(msgs.join(QLatin1Char('\n')), m_numErrors ? KTextEditor::Message::Error : KTextEditor::Message::Warning);
0923     } else if (exitCode != 0) {
0924         buildSuccess = false;
0925         displayBuildResult(i18n("Build failed."), KTextEditor::Message::Warning);
0926     } else {
0927         displayBuildResult(i18n("Build completed without problems."), KTextEditor::Message::Positive);
0928     }
0929 
0930     if (m_buildCancelled) {
0931         buildStatus =
0932             i18n("Build <b>%1 canceled</b>. %2 error(s), %3 warning(s), %4 note(s)", m_currentlyBuildingTarget, m_numErrors, m_numWarnings, m_numNotes);
0933     }
0934     m_buildUi.buildStatusLabel->setText(buildStatus);
0935 
0936     if (buildSuccess && m_runAfterBuild) {
0937         m_runAfterBuild = false;
0938         slotRunAfterBuild();
0939     }
0940 }
0941 
0942 void KateBuildView::slotRunAfterBuild()
0943 {
0944     if (!m_previousIndex.isValid()) {
0945         return;
0946     }
0947     QModelIndex idx = m_previousIndex;
0948     QModelIndex runIdx = idx.siblingAtColumn(2);
0949     const QString runCmd = runIdx.data().toString();
0950     if (runCmd.isEmpty()) {
0951         // Nothing to run, and not a problem
0952         return;
0953     }
0954     const QString workDir = idx.data(TargetModel::WorkDirRole).toString();
0955     if (workDir.isEmpty()) {
0956         displayBuildResult(i18n("Cannot execute: %1 No working directory set.", runCmd), KTextEditor::Message::Warning);
0957         return;
0958     }
0959     QModelIndex nameIdx = idx.siblingAtColumn(0);
0960     QString name = nameIdx.data().toString();
0961 
0962     AppOutput *out = nullptr;
0963     for (int i = 2; i < m_buildUi.u_tabWidget->count(); ++i) {
0964         QString tabToolTip = m_buildUi.u_tabWidget->tabToolTip(i);
0965         if (runCmd == tabToolTip) {
0966             out = qobject_cast<AppOutput *>(m_buildUi.u_tabWidget->widget(i));
0967             if (!out || !out->runningProcess().isEmpty()) {
0968                 out = nullptr;
0969                 continue;
0970             }
0971             // We have a winner, re-use this tab
0972             m_buildUi.u_tabWidget->setCurrentIndex(i);
0973             break;
0974         }
0975     }
0976     if (!out) {
0977         // This is means we create a new tab
0978         out = new AppOutput();
0979         int tabIndex = m_buildUi.u_tabWidget->addTab(out, name);
0980         m_buildUi.u_tabWidget->setCurrentIndex(tabIndex);
0981         m_buildUi.u_tabWidget->setTabToolTip(tabIndex, runCmd);
0982         m_buildUi.u_tabWidget->setTabIcon(tabIndex, QIcon::fromTheme(QStringLiteral("media-playback-start")));
0983 
0984         connect(out, &AppOutput::runningChanged, this, [this]() {
0985             // Update the tab icon when the run state changes
0986             for (int i = 2; i < m_buildUi.u_tabWidget->count(); ++i) {
0987                 AppOutput *tabOut = qobject_cast<AppOutput *>(m_buildUi.u_tabWidget->widget(i));
0988                 if (!tabOut) {
0989                     continue;
0990                 }
0991                 if (!tabOut->runningProcess().isEmpty()) {
0992                     m_buildUi.u_tabWidget->setTabIcon(i, QIcon::fromTheme(QStringLiteral("media-playback-start")));
0993                 } else {
0994                     m_buildUi.u_tabWidget->setTabIcon(i, QIcon::fromTheme(QStringLiteral("media-playback-pause")));
0995                 }
0996             }
0997         });
0998     }
0999 
1000     out->setWorkingDir(workDir);
1001     out->runCommand(runCmd);
1002 
1003     if (m_win->activeView()) {
1004         m_win->activeView()->setFocus();
1005     }
1006 }
1007 
1008 QString KateBuildView::toOutputHtml(const KateBuildView::OutputLine &out)
1009 {
1010     QString htmlStr;
1011     if (!out.file.isEmpty()) {
1012         htmlStr += QStringLiteral("<a href=\"%1:%2:%3\">").arg(out.file).arg(out.lineNr).arg(out.column);
1013     }
1014     switch (out.category) {
1015     case Category::Error:
1016         htmlStr += QStringLiteral("<span class=\"err-text\">");
1017         break;
1018     case Category::Warning:
1019         htmlStr += QStringLiteral("<span class=\"warn-text\">");
1020         break;
1021     case Category::Info:
1022         htmlStr += QStringLiteral("<span class=\"note-text\">");
1023         break;
1024     case Category::Normal:
1025         htmlStr += QStringLiteral("<span>");
1026         break;
1027     }
1028     htmlStr += out.lineStr.toHtmlEscaped();
1029     htmlStr += QStringLiteral("</span>\n");
1030     if (!out.file.isEmpty()) {
1031         htmlStr += QStringLiteral("</a>");
1032     }
1033     return htmlStr;
1034 }
1035 
1036 void KateBuildView::updateTextBrowser()
1037 {
1038     QTextBrowser *edit = m_buildUi.textBrowser;
1039     // Get the scroll position to restore it if not at the end
1040     int scrollValue = edit->verticalScrollBar()->value();
1041     int scrollMax = edit->verticalScrollBar()->maximum();
1042     // Save the selection and restore it after adding the text
1043     QTextCursor cursor = edit->textCursor();
1044 
1045     // set the new document
1046     edit->setHtml(m_htmlOutput);
1047 
1048     // Restore selection and scroll position
1049     edit->setTextCursor(cursor);
1050     if (scrollValue != scrollMax) {
1051         // We had stopped scrolling already
1052         edit->verticalScrollBar()->setValue(scrollValue);
1053         return;
1054     }
1055 
1056     // We were at the bottom before adding lines.
1057     int newPos = edit->verticalScrollBar()->maximum();
1058     if (m_scrollStopPos != -1) {
1059         int targetPos = ((newPos + edit->verticalScrollBar()->pageStep()) * m_scrollStopPos) / m_numOutputLines;
1060         if (targetPos < newPos) {
1061             newPos = targetPos;
1062             // if we want to continue scrolling, just scroll to the end and it will continue
1063             m_scrollStopPos = -1;
1064         }
1065     }
1066     edit->verticalScrollBar()->setValue(newPos);
1067 }
1068 
1069 /******************************************************************/
1070 void KateBuildView::slotReadReadyStdOut()
1071 {
1072     // read data from procs stdout and add
1073     // the text to the end of the output
1074 
1075     // FIXME unify the parsing of stdout and stderr
1076     QString l = QString::fromUtf8(m_proc.readAllStandardOutput());
1077     l.remove(QLatin1Char('\r'));
1078     m_stdOut += l;
1079 
1080     // handle one line at a time
1081     int end = -1;
1082     while ((end = m_stdOut.indexOf(QLatin1Char('\n'))) >= 0) {
1083         const QString line = m_stdOut.mid(0, end);
1084 
1085         // Check if this is a new directory for Make
1086         QRegularExpressionMatch match = m_newDirDetector.match(line);
1087         if (match.hasMatch()) {
1088             QString newDir = match.captured(1);
1089             if ((m_makeDirStack.size() > 1) && (m_makeDirStack.top() == newDir)) {
1090                 m_makeDirStack.pop();
1091                 newDir = m_makeDirStack.top();
1092             } else {
1093                 m_makeDirStack.push(newDir);
1094             }
1095             m_makeDir = newDir;
1096         }
1097 
1098         // Add the new output to the output and possible error/warnings to the diagnostics output
1099         KateBuildView::OutputLine out = processOutputLine(line);
1100         m_htmlOutput += toOutputHtml(out);
1101         m_numOutputLines++;
1102         if (out.category != Category::Normal) {
1103             addError(out);
1104             if (m_scrollStopPos == -1) {
1105                 // stop the scroll a couple of lines before the top of the view
1106                 m_scrollStopPos = std::max(m_numOutputLines - 4, 0);
1107             }
1108         }
1109         m_stdOut.remove(0, end + 1);
1110     }
1111     if (!m_outputTimer.isActive()) {
1112         m_outputTimer.start();
1113     }
1114 }
1115 
1116 /******************************************************************/
1117 void KateBuildView::slotReadReadyStdErr()
1118 {
1119     // FIXME This works for utf8 but not for all charsets
1120     QString l = QString::fromUtf8(m_proc.readAllStandardError());
1121     l.remove(QLatin1Char('\r'));
1122     m_stdErr += l;
1123 
1124     int end = -1;
1125     while ((end = m_stdErr.indexOf(QLatin1Char('\n'))) >= 0) {
1126         const QString line = m_stdErr.mid(0, end);
1127         KateBuildView::OutputLine out = processOutputLine(line);
1128         m_htmlOutput += toOutputHtml(out);
1129         m_numOutputLines++;
1130         if (out.category != Category::Normal) {
1131             addError(out);
1132             if (m_scrollStopPos == -1) {
1133                 // stop the scroll a couple of lines before the top of the view
1134                 // a small improvement could be achieved by storing the number of lines added...
1135                 m_scrollStopPos = std::max(m_numOutputLines - 4, 0);
1136             }
1137         }
1138         m_stdErr.remove(0, end + 1);
1139     }
1140     if (!m_outputTimer.isActive()) {
1141         m_outputTimer.start();
1142     }
1143 }
1144 
1145 /******************************************************************/
1146 KateBuildView::OutputLine KateBuildView::processOutputLine(QString line)
1147 {
1148     // look for a filename
1149     QRegularExpressionMatch match = m_filenameDetector.match(line);
1150 
1151     if (!match.hasMatch()) {
1152         return {Category::Normal, line, line, QString(), 0, 0};
1153     }
1154 
1155     QString filename = match.captured(QStringLiteral("filename"));
1156     const QString line_n = match.captured(QStringLiteral("line"));
1157     const QString col_n = match.captured(QStringLiteral("column"));
1158     const QString msg = match.captured(QStringLiteral("message"));
1159 
1160 #ifdef Q_OS_WIN
1161     // convert '\' to '/' so the concatenation works
1162     filename = QFileInfo(filename).filePath();
1163 #endif
1164 
1165     // qDebug() << "File Name:"<<m_makeDir << filename << " msg:"<< msg;
1166     // add path to file
1167     if (QFile::exists(m_makeDir + QLatin1Char('/') + filename)) {
1168         filename = m_makeDir + QLatin1Char('/') + filename;
1169     }
1170 
1171     // If we still do not have a file name try the extra search paths
1172     int i = 1;
1173     while (!QFile::exists(filename) && i < m_searchPaths.size()) {
1174         if (QFile::exists(m_searchPaths[i] + QLatin1Char('/') + filename)) {
1175             filename = m_searchPaths[i] + QLatin1Char('/') + filename;
1176         }
1177         i++;
1178     }
1179 
1180     Category category = Category::Normal;
1181     static QRegularExpression errorRegExp(QStringLiteral("error:"), QRegularExpression::CaseInsensitiveOption);
1182     static QRegularExpression errorRegExpTr(QStringLiteral("%1:").arg(i18nc("The same word as 'gcc' uses for an error.", "error")),
1183                                             QRegularExpression::CaseInsensitiveOption);
1184     static QRegularExpression warningRegExp(QStringLiteral("warning:"), QRegularExpression::CaseInsensitiveOption);
1185     static QRegularExpression warningRegExpTr(QStringLiteral("%1:").arg(i18nc("The same word as 'gcc' uses for a warning.", "warning")),
1186                                               QRegularExpression::CaseInsensitiveOption);
1187     static QRegularExpression infoRegExp(QStringLiteral("(info|note):"), QRegularExpression::CaseInsensitiveOption);
1188     static QRegularExpression infoRegExpTr(QStringLiteral("(%1):").arg(i18nc("The same words as 'gcc' uses for note or info.", "note|info")),
1189                                            QRegularExpression::CaseInsensitiveOption);
1190     if (msg.contains(errorRegExp) || msg.contains(errorRegExpTr) || msg.contains(QLatin1String("undefined reference"))
1191         || msg.contains(i18nc("The same word as 'ld' uses to mark an ...", "undefined reference"))) {
1192         category = Category::Error;
1193     } else if (msg.contains(warningRegExp) || msg.contains(warningRegExpTr)) {
1194         category = Category::Warning;
1195     } else if (msg.contains(infoRegExp) || msg.contains(infoRegExpTr)) {
1196         category = Category::Info;
1197     }
1198     // Now we have the data we need show the error/warning
1199     return {category, line, msg, filename, line_n.toInt(), col_n.toInt()};
1200 }
1201 
1202 /******************************************************************/
1203 void KateBuildView::slotAddTargetClicked()
1204 {
1205     QModelIndex current = m_targetsUi->targetsView->currentIndex();
1206     QString currName = DefTargetName;
1207     QString currCmd;
1208     QString currRun;
1209 
1210     current = m_targetsUi->proxyModel.mapToSource(current);
1211     QModelIndex index = m_targetsUi->targetsModel.addCommandAfter(current, currName, currCmd, currRun);
1212     index = m_targetsUi->proxyModel.mapFromSource(index);
1213     m_targetsUi->targetsView->setCurrentIndex(index);
1214     if (index.data(TargetModel::IsProjectTargetRole).toBool()) {
1215         saveProjectTargets();
1216     }
1217 }
1218 
1219 /******************************************************************/
1220 void KateBuildView::targetSetNew()
1221 {
1222     m_targetsUi->targetFilterEdit->setText(QString());
1223     QModelIndex currentIndex = m_targetsUi->proxyModel.mapToSource(m_targetsUi->targetsView->currentIndex());
1224     QModelIndex index = m_targetsUi->targetsModel.insertTargetSetAfter(currentIndex, i18n("Target Set"), QString());
1225     QModelIndex buildIndex = m_targetsUi->targetsModel.addCommandAfter(index, i18n("Build"), DefBuildCmd, QString());
1226     m_targetsUi->targetsModel.addCommandAfter(index, i18n("Clean"), DefCleanCmd, QString());
1227     m_targetsUi->targetsModel.addCommandAfter(index, i18n("Config"), DefConfigCmd, QString());
1228     m_targetsUi->targetsModel.addCommandAfter(index, i18n("ConfigClean"), DefConfClean, QString());
1229     buildIndex = m_targetsUi->proxyModel.mapFromSource(buildIndex);
1230     m_targetsUi->targetsView->setCurrentIndex(buildIndex);
1231     if (index.data(TargetModel::IsProjectTargetRole).toBool()) {
1232         saveProjectTargets();
1233     }
1234 }
1235 
1236 /******************************************************************/
1237 void KateBuildView::targetOrSetCopy()
1238 {
1239     QModelIndex currentIndex = m_targetsUi->targetsView->currentIndex();
1240     currentIndex = m_targetsUi->proxyModel.mapToSource(currentIndex);
1241     m_targetsUi->targetFilterEdit->setText(QString());
1242     QModelIndex newIndex = m_targetsUi->targetsModel.copyTargetOrSet(currentIndex);
1243     if (m_targetsUi->targetsModel.hasChildren(newIndex)) {
1244         newIndex = m_targetsUi->proxyModel.mapFromSource(newIndex);
1245         m_targetsUi->targetsView->setCurrentIndex(newIndex.model()->index(0, 0, newIndex));
1246         return;
1247     }
1248     newIndex = m_targetsUi->proxyModel.mapFromSource(newIndex);
1249     m_targetsUi->targetsView->setCurrentIndex(newIndex);
1250     if (newIndex.data(TargetModel::IsProjectTargetRole).toBool()) {
1251         saveProjectTargets();
1252     }
1253 }
1254 
1255 /******************************************************************/
1256 void KateBuildView::targetDelete()
1257 {
1258     QModelIndex currentIndex = m_targetsUi->targetsView->currentIndex();
1259     currentIndex = m_targetsUi->proxyModel.mapToSource(currentIndex);
1260     bool wasProjectTarget = currentIndex.data(TargetModel::IsProjectTargetRole).toBool();
1261     m_targetsUi->targetsModel.deleteItem(currentIndex);
1262 
1263     if (m_targetsUi->targetsModel.rowCount() == 0) {
1264         targetSetNew();
1265     }
1266     if (wasProjectTarget) {
1267         saveProjectTargets();
1268     }
1269 }
1270 
1271 /******************************************************************/
1272 void KateBuildView::slotPluginViewCreated(const QString &name, QObject *pluginView)
1273 {
1274     // add view
1275     if (pluginView && name == QLatin1String("kateprojectplugin")) {
1276         m_projectPluginView = pluginView;
1277         addProjectTarget();
1278         connect(pluginView, SIGNAL(projectMapChanged()), this, SLOT(slotProjectMapChanged()), Qt::UniqueConnection);
1279     }
1280 }
1281 
1282 /******************************************************************/
1283 void KateBuildView::slotPluginViewDeleted(const QString &name, QObject *)
1284 {
1285     // remove view
1286     if (name == QLatin1String("kateprojectplugin")) {
1287         m_projectPluginView = nullptr;
1288         m_targetsUi->targetsModel.deleteProjectTargerts();
1289     }
1290 }
1291 
1292 /******************************************************************/
1293 void KateBuildView::slotProjectMapChanged()
1294 {
1295     // only do stuff with valid project
1296     if (!m_projectPluginView) {
1297         return;
1298     }
1299     m_targetsUi->targetsModel.deleteProjectTargerts();
1300     addProjectTarget();
1301 }
1302 
1303 /******************************************************************/
1304 void KateBuildView::addProjectTarget()
1305 {
1306     // only do stuff with a valid project
1307     if (!m_projectPluginView) {
1308         return;
1309     }
1310 
1311     // Delete any old project plugin targets
1312     m_targetsUi->targetsModel.deleteProjectTargerts();
1313 
1314     const QModelIndex projRootIndex = m_targetsUi->targetsModel.projectRootIndex();
1315     const QString projectsBaseDir = m_projectPluginView->property("projectBaseDir").toString();
1316 
1317     // Read the targets from the override if available
1318     const QString userOverride = projectsBaseDir + QStringLiteral("/.kateproject.build");
1319     if (QFile::exists(userOverride)) {
1320         // We have user modified commands
1321         QFile file(userOverride);
1322         if (file.open(QIODevice::ReadOnly)) {
1323             bool targetAdded = false;
1324             QJsonParseError error;
1325             const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
1326             file.close();
1327             const QJsonObject obj = doc.object();
1328             const QJsonArray sets = obj[QStringLiteral("target_sets")].toArray();
1329             for (const auto &setVal : sets) {
1330                 const auto obj = setVal.toObject();
1331                 const QString setName = obj[QStringLiteral("name")].toString();
1332                 const QString workDir = obj[QStringLiteral("directory")].toString();
1333                 const QModelIndex setIdx = m_targetsUi->targetsModel.insertTargetSetAfter(projRootIndex, setName, workDir);
1334                 for (const auto &targetVals : obj[QStringLiteral("targets")].toArray()) {
1335                     const auto tgtObj = targetVals.toObject();
1336                     const QString name = tgtObj[QStringLiteral("name")].toString();
1337                     const QString buildCmd = tgtObj[QStringLiteral("build_cmd")].toString();
1338                     const QString runCmd = tgtObj[QStringLiteral("run_cmd")].toString();
1339                     if (name.isEmpty()) {
1340                         continue;
1341                     }
1342                     m_targetsUi->targetsModel.addCommandAfter(setIdx, name, buildCmd, runCmd);
1343                     targetAdded = true;
1344                 }
1345                 if (targetAdded) {
1346                     const auto index = m_targetsUi->proxyModel.mapFromSource(setIdx);
1347                     if (index.isValid()) {
1348                         m_targetsUi->targetsView->expand(index);
1349                     }
1350                 }
1351             }
1352             if (targetAdded) {
1353                 return;
1354                 // The user file overrides the rest
1355                 // TODO maybe add heuristics for when to re-read the project data
1356             }
1357         }
1358     }
1359 
1360     // query new project map
1361     QVariantMap projectMap = m_projectPluginView->property("projectMap").toMap();
1362 
1363     // do we have a valid map for build settings?
1364     QVariantMap buildMap = projectMap.value(QStringLiteral("build")).toMap();
1365     if (buildMap.isEmpty()) {
1366         return;
1367     }
1368 
1369     // handle build directory as relative to project file, if possible, see bug 413306
1370     QString projectsBuildDir = buildMap.value(QStringLiteral("directory")).toString();
1371     QString projectName = m_projectPluginView->property("projectName").toString();
1372     if (!projectsBaseDir.isEmpty()) {
1373         projectsBuildDir = QDir(projectsBaseDir).absoluteFilePath(projectsBuildDir);
1374     }
1375 
1376     const QModelIndex set = m_targetsUi->targetsModel.insertTargetSetAfter(projRootIndex, projectName, projectsBuildDir);
1377     const QString defaultTarget = buildMap.value(QStringLiteral("default_target")).toString();
1378 
1379     const QVariantList targetsets = buildMap.value(QStringLiteral("targets")).toList();
1380     for (const QVariant &targetVariant : targetsets) {
1381         const QVariantMap targetMap = targetVariant.toMap();
1382         const QString tgtName = targetMap[QStringLiteral("name")].toString();
1383         const QString buildCmd = targetMap[QStringLiteral("build_cmd")].toString();
1384         const QString runCmd = targetMap[QStringLiteral("run_cmd")].toString();
1385 
1386         if (tgtName.isEmpty() || buildCmd.isEmpty()) {
1387             continue;
1388         }
1389         QPersistentModelIndex idx = m_targetsUi->targetsModel.addCommandAfter(set, tgtName, buildCmd, runCmd);
1390         if (tgtName == defaultTarget) {
1391             // A bit of backwards compatibility, move the "default" target to the top
1392             while (idx.row() > 0) {
1393                 m_targetsUi->targetsModel.moveRowUp(idx);
1394             }
1395         }
1396     }
1397 
1398     const auto index = m_targetsUi->proxyModel.mapFromSource(set);
1399     if (index.isValid()) {
1400         m_targetsUi->targetsView->expand(index);
1401     }
1402 
1403     // FIXME read CMakePresets.json for more build target sets
1404 }
1405 
1406 /******************************************************************/
1407 void KateBuildView::saveProjectTargets()
1408 {
1409     // only do stuff with a valid project
1410     if (!m_projectPluginView) {
1411         return;
1412     }
1413 
1414     const QString projectsBaseDir = m_projectPluginView->property("projectBaseDir").toString();
1415     const QString userOverride = projectsBaseDir + QStringLiteral("/.kateproject.build");
1416 
1417     QFile file(userOverride);
1418     if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
1419         displayMessage(i18n("Cannot save build targets in: %1", userOverride), KTextEditor::Message::Error);
1420         return;
1421     }
1422 
1423     const QList<TargetModel::TargetSet> targets = m_targetsUi->targetsModel.projectTargetSets();
1424 
1425     QJsonObject root;
1426     root[QStringLiteral("Auto_generated")] = QStringLiteral("This file is auto-generated. Any extra tags or formatting will be lost");
1427 
1428     QJsonArray setArray;
1429     for (const auto &set : targets) {
1430         QJsonObject setObj;
1431         setObj[QStringLiteral("name")] = set.name;
1432         setObj[QStringLiteral("directory")] = set.workDir;
1433         QJsonArray cmdArray;
1434         for (const auto &cmd : std::as_const(set.commands)) {
1435             QJsonObject cmdObj;
1436             cmdObj[QStringLiteral("name")] = cmd.name;
1437             cmdObj[QStringLiteral("build_cmd")] = cmd.buildCmd;
1438             cmdObj[QStringLiteral("runCmd")] = cmd.runCmd;
1439             cmdArray.append(cmdObj);
1440         }
1441         setObj[QStringLiteral("targets")] = cmdArray;
1442         setArray.append(setObj);
1443     }
1444     root[QStringLiteral("target_sets")] = setArray;
1445     QJsonDocument doc(root);
1446 
1447     file.write(doc.toJson());
1448     file.close();
1449 }
1450 
1451 /******************************************************************/
1452 bool KateBuildView::eventFilter(QObject *obj, QEvent *event)
1453 {
1454     switch (event->type()) {
1455     case QEvent::KeyPress: {
1456         QKeyEvent *ke = static_cast<QKeyEvent *>(event);
1457         if ((obj == m_toolView) && (ke->key() == Qt::Key_Escape)) {
1458             m_win->hideToolView(m_toolView);
1459             event->accept();
1460             return true;
1461         }
1462         break;
1463     }
1464     case QEvent::ShortcutOverride: {
1465         QKeyEvent *ke = static_cast<QKeyEvent *>(event);
1466         if (ke->matches(QKeySequence::Copy)) {
1467             m_buildUi.textBrowser->copy();
1468             event->accept();
1469             return true;
1470         } else if (ke->matches(QKeySequence::SelectAll)) {
1471             m_buildUi.textBrowser->selectAll();
1472             event->accept();
1473             return true;
1474         }
1475         break;
1476     }
1477     case QEvent::KeyRelease: {
1478         QKeyEvent *ke = static_cast<QKeyEvent *>(event);
1479         if (ke->matches(QKeySequence::Copy) || ke->matches(QKeySequence::SelectAll)) {
1480             event->accept();
1481             return true;
1482         }
1483         break;
1484     }
1485     default: {
1486     }
1487     }
1488     return QObject::eventFilter(obj, event);
1489 }
1490 
1491 /******************************************************************/
1492 void KateBuildView::handleEsc(QEvent *e)
1493 {
1494     if (!m_win) {
1495         return;
1496     }
1497 
1498     QKeyEvent *k = static_cast<QKeyEvent *>(e);
1499     if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {
1500         if (m_toolView->isVisible()) {
1501             m_win->hideToolView(m_toolView);
1502         }
1503     }
1504 }
1505 
1506 #include "moc_plugin_katebuild.cpp"
1507 #include "plugin_katebuild.moc"
1508 
1509 // kate: space-indent on; indent-width 4; replace-tabs on;