plugin_katebuild.cpp 49.1 KB
Newer Older
Kåre Särs's avatar
Kåre Särs committed
1 2
/* plugin_katebuild.c                    Kate Plugin
**
3
** Copyright (C) 2013 by Alexander Neundorf <neundorf@kde.org>
4
** Copyright (C) 2006-2015 by Kåre Särs <kare.sars@iki.fi>
5
** Copyright (C) 2011 by Ian Wakeling <ian.wakeling@ntlworld.com>
Kåre Särs's avatar
Kåre Särs committed
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
**
** This code is mostly a modification of the GPL'ed Make plugin
** by Adriaan de Groot.
*/

/*
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program in a file called COPYING; if not, write to
** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
** MA 02110-1301, USA.
*/

28
#include "plugin_katebuild.h"
Kåre Särs's avatar
Kåre Särs committed
29 30 31

#include <cassert>

32
#include <QCompleter>
33
#include <QDir>
34
#include <QDirModel>
35
#include <QFileDialog>
36
#include <QFileInfo>
37
#include <QIcon>
38 39 40 41
#include <QKeyEvent>
#include <QRegularExpressionMatch>
#include <QScrollBar>
#include <QString>
42 43

#include <QAction>
44

45
#include <KActionCollection>
46 47
#include <KTextEditor/Application>
#include <KXMLGUIFactory>
Kåre Särs's avatar
Kåre Särs committed
48 49 50

#include <kmessagebox.h>

51
#include <kaboutdata.h>
52
#include <klocalizedstring.h>
53 54
#include <kpluginfactory.h>
#include <kpluginloader.h>
Kåre Särs's avatar
Kåre Särs committed
55

56
#include <ktexteditor/markinterface.h>
57
#include <ktexteditor/movinginterface.h>
58
#include <ktexteditor_version.h>
59

60
#include "SelectTargetView.h"
61

62
K_PLUGIN_FACTORY_WITH_JSON(KateBuildPluginFactory, "katebuildplugin.json", registerPlugin<KateBuildPlugin>();)
Kåre Särs's avatar
Kåre Särs committed
63

64 65 66 67 68
static const QString DefConfigCmd = QStringLiteral("cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr/local ../");
static const QString DefConfClean;
static const QString DefTargetName = QStringLiteral("all");
static const QString DefBuildCmd = QStringLiteral("make");
static const QString DefCleanCmd = QStringLiteral("make clean");
69
static const QString NinjaPrefix = QStringLiteral("[ninja]");
70

71 72
static QIcon messageIcon(KateBuildView::ErrorCategory severity)
{
73 74 75 76 77 78
    // clang-format off
#define RETURN_CACHED_ICON(name, fallbackname) \
    { \
        static QIcon icon(QIcon::fromTheme(QStringLiteral(name), \
                                           QIcon::fromTheme(QStringLiteral(fallbackname)))); \
        return icon; \
79
    }
80
    // clang-format on
81
    switch (severity) {
82
    case KateBuildView::CategoryError:
83
        RETURN_CACHED_ICON("data-error", "dialog-error")
84
    case KateBuildView::CategoryWarning:
85
        RETURN_CACHED_ICON("data-warning", "dialog-warning")
86 87
    default:
        break;
88 89 90 91
    }
    return QIcon();
}

92
struct ItemData {
93 94 95 96 97 98
    // ensure destruction, but not inadvertently so by a variant value copy
    QSharedPointer<KTextEditor::MovingCursor> cursor;
};

Q_DECLARE_METATYPE(ItemData)

Kåre Särs's avatar
Kåre Särs committed
99
/******************************************************************/
100 101
KateBuildPlugin::KateBuildPlugin(QObject *parent, const VariantList &)
    : KTextEditor::Plugin(parent)
Kåre Särs's avatar
Kåre Särs committed
102
{
103
    // KF5 FIXME KGlobal::locale()->insertCatalog("katebuild-plugin");
Kåre Särs's avatar
Kåre Särs committed
104 105 106
}

/******************************************************************/
107
QObject *KateBuildPlugin::createView(KTextEditor::MainWindow *mainWindow)
Kåre Särs's avatar
Kåre Särs committed
108
{
109
    return new KateBuildView(this, mainWindow);
Kåre Särs's avatar
Kåre Särs committed
110 111 112
}

/******************************************************************/
113
KateBuildView::KateBuildView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mw)
114
    : QObject(mw)
115
    , m_win(mw)
116
    , m_buildWidget(nullptr)
117
    , m_outputWidgetWidth(0)
118
    , m_proc(this)
119 120
    , m_stdOut()
    , m_stdErr()
121
    , m_buildCancelled(false)
122
    , m_displayModeBeforeBuild(1)
123
    // NOTE this will not allow spaces in file names.
124
    // e.g. from gcc: "main.cpp:14: error: cannot convert ‘std::string’ to ‘int’ in return"
125
    // e.g. from gcc: "main.cpp:14:8: error: cannot convert ‘std::string’ to ‘int’ in return"
126
    // e.g. from icpc: "main.cpp(14): error: no suitable conversion function from "std::string" to "int" exists"
127 128
    // e.g. from clang: ""main.cpp(14,8): fatal error: 'boost/scoped_array.hpp' file not found"
    , m_filenameDetector(QStringLiteral("((?:[a-np-zA-Z]:[\\\\/])?[\\.a-zA-Z0-9\\\\/\\-_]+\\.[a-zA-Z0-9]+)[:\\(](\\d+)[,:]?(\\d+)?[\\):]* (.*)"))
129
    , m_newDirDetector(QStringLiteral("make\\[.+\\]: .+ '(.*)'"))
Kåre Särs's avatar
Kåre Särs committed
130
{
131
    KXMLGUIClient::setComponentName(QStringLiteral("katebuild"), i18n("Kate Build Plugin"));
Laurent Montel's avatar
Laurent Montel committed
132
    setXMLFile(QStringLiteral("ui.rc"));
133

134
    m_toolView = mw->createToolView(plugin, QStringLiteral("kate_plugin_katebuildplugin"), KTextEditor::MainWindow::Bottom, QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")), i18n("Build Output"));
135

136 137
    QAction *a = actionCollection()->addAction(QStringLiteral("select_target"));
    a->setText(i18n("Select Target..."));
Bernhard Beschow's avatar
Bernhard Beschow committed
138
    a->setIcon(QIcon::fromTheme(QStringLiteral("select")));
139
    connect(a, &QAction::triggered, this, &KateBuildView::slotSelectTarget);
140
    a = actionCollection()->addAction(QStringLiteral("build_default_target"));
141
    a->setText(i18n("Build Default Target"));
142
    connect(a, &QAction::triggered, this, &KateBuildView::slotBuildDefaultTarget);
143

144 145
    a = actionCollection()->addAction(QStringLiteral("build_previous_target"));
    a->setText(i18n("Build Previous Target"));
146
    connect(a, &QAction::triggered, this, &KateBuildView::slotBuildPreviousTarget);
147

148
    a = actionCollection()->addAction(QStringLiteral("stop"));
149
    a->setText(i18n("Stop"));
Bernhard Beschow's avatar
Bernhard Beschow committed
150
    a->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
151
    connect(a, &QAction::triggered, this, &KateBuildView::slotStop);
152

153
    a = actionCollection()->addAction(QStringLiteral("goto_next"));
154
    a->setText(i18n("Next Error"));
Bernhard Beschow's avatar
Bernhard Beschow committed
155
    a->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
156
    actionCollection()->setDefaultShortcut(a, Qt::SHIFT + Qt::ALT + Qt::Key_Right);
157
    connect(a, &QAction::triggered, this, &KateBuildView::slotNext);
158

159
    a = actionCollection()->addAction(QStringLiteral("goto_prev"));
160
    a->setText(i18n("Previous Error"));
Bernhard Beschow's avatar
Bernhard Beschow committed
161
    a->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
162
    actionCollection()->setDefaultShortcut(a, Qt::SHIFT + Qt::ALT + Qt::Key_Left);
163
    connect(a, &QAction::triggered, this, &KateBuildView::slotPrev);
164

165 166 167 168
    m_showMarks = a = actionCollection()->addAction(QStringLiteral("show_marks"));
    a->setText(i18n("Show Marks"));
    a->setCheckable(true);
    connect(a, &QAction::triggered, this, &KateBuildView::slotDisplayOption);
169

170 171
    m_buildWidget = new QWidget(m_toolView);
    m_buildUi.setupUi(m_buildWidget);
172
    m_targetsUi = new TargetsUi(this, m_buildUi.u_tabWidget);
173
    m_buildUi.u_tabWidget->insertTab(0, m_targetsUi, i18nc("Tab label", "Target Settings"));
174
    m_buildUi.u_tabWidget->setCurrentWidget(m_targetsUi);
Kåre Särs's avatar
Kåre Särs committed
175

176 177 178 179 180 181 182 183 184
    m_buildWidget->installEventFilter(this);

    m_buildUi.buildAgainButton->setVisible(true);
    m_buildUi.cancelBuildButton->setVisible(true);
    m_buildUi.buildStatusLabel->setVisible(true);
    m_buildUi.buildAgainButton2->setVisible(false);
    m_buildUi.cancelBuildButton2->setVisible(false);
    m_buildUi.buildStatusLabel2->setVisible(false);
    m_buildUi.extraLineLayout->setAlignment(Qt::AlignRight);
185 186
    m_buildUi.cancelBuildButton->setEnabled(false);
    m_buildUi.cancelBuildButton2->setEnabled(false);
Kåre Särs's avatar
Kåre Särs committed
187

188
    connect(m_buildUi.errTreeWidget, &QTreeWidget::itemClicked, this, &KateBuildView::slotErrorSelected);
Kåre Särs's avatar
Kåre Särs committed
189

190
    m_buildUi.plainTextEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
191
    m_buildUi.plainTextEdit->setReadOnly(true);
192
    slotDisplayMode(FullOutput);
Kåre Särs's avatar
Kåre Särs committed
193

194
    connect(m_buildUi.displayModeSlider, &QSlider::valueChanged, this, &KateBuildView::slotDisplayMode);
195

196 197 198 199
    connect(m_buildUi.buildAgainButton, &QPushButton::clicked, this, &KateBuildView::slotBuildPreviousTarget);
    connect(m_buildUi.cancelBuildButton, &QPushButton::clicked, this, &KateBuildView::slotStop);
    connect(m_buildUi.buildAgainButton2, &QPushButton::clicked, this, &KateBuildView::slotBuildPreviousTarget);
    connect(m_buildUi.cancelBuildButton2, &QPushButton::clicked, this, &KateBuildView::slotStop);
200

201 202 203
    connect(m_targetsUi->newTarget, &QToolButton::clicked, this, &KateBuildView::targetSetNew);
    connect(m_targetsUi->copyTarget, &QToolButton::clicked, this, &KateBuildView::targetOrSetCopy);
    connect(m_targetsUi->deleteTarget, &QToolButton::clicked, this, &KateBuildView::targetDelete);
Kåre Särs's avatar
Kåre Särs committed
204

205 206 207
    connect(m_targetsUi->addButton, &QToolButton::clicked, this, &KateBuildView::slotAddTargetClicked);
    connect(m_targetsUi->buildButton, &QToolButton::clicked, this, &KateBuildView::slotBuildActiveTarget);
    connect(m_targetsUi, &TargetsUi::enterPressed, this, &KateBuildView::slotBuildActiveTarget);
208

209
    m_proc.setOutputChannelMode(KProcess::SeparateChannels);
210 211 212
    connect(&m_proc, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &KateBuildView::slotProcExited);
    connect(&m_proc, &KProcess::readyReadStandardError, this, &KateBuildView::slotReadReadyStdErr);
    connect(&m_proc, &KProcess::readyReadStandardOutput, this, &KateBuildView::slotReadReadyStdOut);
Kåre Särs's avatar
Kåre Särs committed
213

214
    connect(m_win, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KateBuildView::handleEsc);
215
    connect(m_win, &KTextEditor::MainWindow::viewChanged, this, &KateBuildView::slotViewChanged);
216

217 218
    m_toolView->installEventFilter(this);

219
    m_win->guiFactory()->addClient(this);
220 221

    // watch for project plugin view creation/deletion
222 223
    connect(m_win, &KTextEditor::MainWindow::pluginViewCreated, this, &KateBuildView::slotPluginViewCreated);
    connect(m_win, &KTextEditor::MainWindow::pluginViewDeleted, this, &KateBuildView::slotPluginViewDeleted);
224

225 226 227
    // Connect signals from project plugin to our slots
    m_projectPluginView = m_win->pluginView(QStringLiteral("kateprojectplugin"));
    slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView);
Kåre Särs's avatar
Kåre Särs committed
228 229 230 231 232
}

/******************************************************************/
KateBuildView::~KateBuildView()
{
233
    m_win->guiFactory()->removeClient(this);
Kåre Särs's avatar
Kåre Särs committed
234 235 236 237
    delete m_toolView;
}

/******************************************************************/
238
void KateBuildView::readSessionConfig(const KConfigGroup &cg)
Kåre Särs's avatar
Kåre Särs committed
239
{
240
    int numTargets = cg.readEntry(QStringLiteral("NumTargets"), 0);
241
    m_targetsUi->targetsModel.clear();
242
    int tmpIndex;
243 244
    int tmpCmd;

245
    if (numTargets == 0) {
246
        // either the config is empty or uses the older format
247 248 249 250
        m_targetsUi->targetsModel.addTargetSet(i18n("Target Set"), QString());
        m_targetsUi->targetsModel.addCommand(0, i18n("build"), cg.readEntry(QStringLiteral("Make Command"), DefBuildCmd));
        m_targetsUi->targetsModel.addCommand(0, i18n("clean"), cg.readEntry(QStringLiteral("Clean Command"), DefCleanCmd));
        m_targetsUi->targetsModel.addCommand(0, i18n("config"), DefConfigCmd);
251

252
        QString quickCmd = cg.readEntry(QStringLiteral("Quick Compile Command"));
253
        if (!quickCmd.isEmpty()) {
254
            m_targetsUi->targetsModel.addCommand(0, i18n("quick"), quickCmd);
255
        }
256
        tmpIndex = 0;
257
        tmpCmd = 0;
258 259
    } else {
        for (int i = 0; i < numTargets; i++) {
260
            QStringList targetNames = cg.readEntry(QStringLiteral("%1 Target Names").arg(i), QStringList());
261 262
            QString targetSetName = cg.readEntry(QStringLiteral("%1 Target").arg(i), QString());
            QString buildDir = cg.readEntry(QStringLiteral("%1 BuildPath").arg(i), QString());
263

264
            m_targetsUi->targetsModel.addTargetSet(targetSetName, buildDir);
265

266
            if (targetNames.isEmpty()) {
267
                QString quickCmd = cg.readEntry(QStringLiteral("%1 QuickCmd").arg(i));
268 269
                m_targetsUi->targetsModel.addCommand(i, i18n("build"), cg.readEntry(QStringLiteral("%1 BuildCmd"), DefBuildCmd));
                m_targetsUi->targetsModel.addCommand(i, i18n("clean"), cg.readEntry(QStringLiteral("%1 CleanCmd"), DefCleanCmd));
270
                if (!quickCmd.isEmpty()) {
271
                    m_targetsUi->targetsModel.addCommand(i, i18n("quick"), quickCmd);
272
                }
273
                m_targetsUi->targetsModel.setDefaultCmd(i, i18n("build"));
274 275 276
            } else {
                for (int tn = 0; tn < targetNames.size(); ++tn) {
                    const QString &targetName = targetNames.at(tn);
277
                    m_targetsUi->targetsModel.addCommand(i, targetName, cg.readEntry(QStringLiteral("%1 BuildCmd %2").arg(i).arg(targetName), DefBuildCmd));
278
                }
279 280
                QString defCmd = cg.readEntry(QStringLiteral("%1 Target Default").arg(i), QString());
                m_targetsUi->targetsModel.setDefaultCmd(i, defCmd);
281
            }
Kåre Särs's avatar
Kåre Särs committed
282
        }
283
        tmpIndex = cg.readEntry(QStringLiteral("Active Target Index"), 0);
284
        tmpCmd = cg.readEntry(QStringLiteral("Active Target Command"), 0);
Kåre Särs's avatar
Kåre Särs committed
285
    }
286

287 288 289
    m_targetsUi->targetsView->expandAll();
    m_targetsUi->targetsView->resizeColumnToContents(0);
    m_targetsUi->targetsView->collapseAll();
290

291 292 293
    QModelIndex root = m_targetsUi->targetsModel.index(tmpIndex);
    QModelIndex cmdIndex = m_targetsUi->targetsModel.index(tmpCmd, 0, root);
    m_targetsUi->targetsView->setCurrentIndex(cmdIndex);
294

295 296 297
    auto showMarks = cg.readEntry(QStringLiteral("Show Marks"), false);
    m_showMarks->setChecked(showMarks);

298 299
    // Add project targets, if any
    slotAddProjectTarget();
Kåre Särs's avatar
Kåre Särs committed
300 301 302
}

/******************************************************************/
303
void KateBuildView::writeSessionConfig(KConfigGroup &cg)
Kåre Särs's avatar
Kåre Särs committed
304
{
305
    // Don't save project targets, is not our area of accountability
306
    m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets"));
307

308
    QList<TargetModel::TargetSet> targets = m_targetsUi->targetsModel.targetSets();
309

310
    cg.writeEntry("NumTargets", targets.size());
311

312
    for (int i = 0; i < targets.size(); i++) {
313 314 315
        cg.writeEntry(QStringLiteral("%1 Target").arg(i), targets[i].name);
        cg.writeEntry(QStringLiteral("%1 BuildPath").arg(i), targets[i].workDir);
        QStringList cmdNames;
316

317 318 319
        for (int j = 0; j < targets[i].commands.count(); j++) {
            const QString &cmdName = targets[i].commands[j].first;
            const QString &buildCmd = targets[i].commands[j].second;
320 321
            cmdNames << cmdName;
            cg.writeEntry(QStringLiteral("%1 BuildCmd %2").arg(i).arg(cmdName), buildCmd);
322
        }
323 324 325 326 327 328 329 330
        cg.writeEntry(QStringLiteral("%1 Target Names").arg(i), cmdNames);
        cg.writeEntry(QStringLiteral("%1 Target Default").arg(i), targets[i].defaultCmd);
    }
    int setRow = 0;
    int set = 0;
    QModelIndex ind = m_targetsUi->targetsView->currentIndex();
    if (ind.internalId() == TargetModel::InvalidIndex) {
        set = ind.row();
331
    } else {
332 333 334
        set = ind.internalId();
        setRow = ind.row();
    }
335 336
    if (setRow < 0)
        setRow = 0;
337 338 339

    cg.writeEntry(QStringLiteral("Active Target Index"), set);
    cg.writeEntry(QStringLiteral("Active Target Command"), setRow);
340
    cg.writeEntry(QStringLiteral("Show Marks"), m_showMarks->isChecked());
341 342

    // Restore project targets, if any
343
    slotAddProjectTarget();
Kåre Särs's avatar
Kåre Särs committed
344 345 346 347 348
}

/******************************************************************/
void KateBuildView::slotNext()
{
349
    const int itemCount = m_buildUi.errTreeWidget->topLevelItemCount();
Dominik Haumann's avatar
Dominik Haumann committed
350 351 352
    if (itemCount == 0) {
        return;
    }
Kåre Särs's avatar
Kåre Särs committed
353

354
    QTreeWidgetItem *item = m_buildUi.errTreeWidget->currentItem();
355 356
    if (item && item->isHidden())
        item = nullptr;
Kåre Särs's avatar
Kåre Särs committed
357

358
    int i = (item == nullptr) ? -1 : m_buildUi.errTreeWidget->indexOfTopLevelItem(item);
Kåre Särs's avatar
Kåre Särs committed
359

Dominik Haumann's avatar
Dominik Haumann committed
360
    while (++i < itemCount) {
361
        item = m_buildUi.errTreeWidget->topLevelItem(i);
362 363
        // Search item which fit view settings and has desired data
        if (!item->text(1).isEmpty() && !item->isHidden() && item->data(1, Qt::UserRole).toInt()) {
364
            m_buildUi.errTreeWidget->setCurrentItem(item);
365
            m_buildUi.errTreeWidget->scrollToItem(item);
366
            slotErrorSelected(item);
Dominik Haumann's avatar
Dominik Haumann committed
367
            return;
Kåre Särs's avatar
Kåre Särs committed
368 369 370 371 372 373 374
        }
    }
}

/******************************************************************/
void KateBuildView::slotPrev()
{
375
    const int itemCount = m_buildUi.errTreeWidget->topLevelItemCount();
Dominik Haumann's avatar
Dominik Haumann committed
376 377 378
    if (itemCount == 0) {
        return;
    }
Kåre Särs's avatar
Kåre Särs committed
379

380
    QTreeWidgetItem *item = m_buildUi.errTreeWidget->currentItem();
381 382
    if (item && item->isHidden())
        item = nullptr;
Kåre Särs's avatar
Kåre Särs committed
383

384
    int i = (item == nullptr) ? itemCount : m_buildUi.errTreeWidget->indexOfTopLevelItem(item);
Kåre Särs's avatar
Kåre Särs committed
385

Dominik Haumann's avatar
Dominik Haumann committed
386
    while (--i >= 0) {
387
        item = m_buildUi.errTreeWidget->topLevelItem(i);
388 389
        // Search item which fit view settings and has desired data
        if (!item->text(1).isEmpty() && !item->isHidden() && item->data(1, Qt::UserRole).toInt()) {
390
            m_buildUi.errTreeWidget->setCurrentItem(item);
391
            m_buildUi.errTreeWidget->scrollToItem(item);
392
            slotErrorSelected(item);
Dominik Haumann's avatar
Dominik Haumann committed
393
            return;
Kåre Särs's avatar
Kåre Särs committed
394 395 396 397
        }
    }
}

398
#ifdef Q_OS_WIN
Kåre Särs's avatar
Kåre Särs committed
399
/******************************************************************/
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
QString KateBuildView::caseFixed(const QString &path)
{
    QStringList paths = path.split(QLatin1Char('/'));
    if (paths.isEmpty()) { return path; }

    qDebug() << "PATH=" << paths;

    QString result = paths[0].toUpper() + QLatin1Char('/');
    for (int i=1; i<paths.count(); ++i) {
        QDir curDir(result);
        const QStringList items = curDir.entryList();
        int j;
        for (j=0; j<items.size(); ++j) {
            if (items[j].compare(paths[i], Qt::CaseInsensitive) == 0) {
                qDebug() << "adding" << items[j];
                result += items[j];
                if (i < paths.count()-1) {
                    result += QLatin1Char('/');
                }
                break;
            }
        }
        if (j==items.size()) { return path; }
    }
    return result;
}
#endif

428
void KateBuildView::slotErrorSelected(QTreeWidgetItem *item)
Kåre Särs's avatar
Kåre Särs committed
429
{
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
    // any view active?
    if (!m_win->activeView()) {
        return;
    }

    // Avoid garish highlighting of the selected line
    m_win->activeView()->setFocus();

    // Search the item where the data we need is stored
    while (!item->data(1, Qt::UserRole).toInt()) {
        item = m_buildUi.errTreeWidget->itemAbove(item);
        if (!item) {
            return;
        }
    }

Kåre Särs's avatar
Kåre Särs committed
446
    // get stuff
447
    QString filename = item->data(0, Qt::UserRole).toString();
448 449 450 451
    if (filename.isEmpty()) {
        return;
    }

452 453 454 455 456 457 458 459
    int line = item->data(1, Qt::UserRole).toInt();
    int column = item->data(2, Qt::UserRole).toInt();
    // check with moving cursor
    auto data = item->data(0, DataRole).value<ItemData>();
    if (data.cursor) {
        line = data.cursor->line();
        column = data.cursor->column();
    }
Kåre Särs's avatar
Kåre Särs committed
460

461 462 463 464
#ifdef Q_OS_WIN
    filename = caseFixed(filename);
#endif

465 466
    // Check if the file exists
    if (!QFileInfo::exists(filename)) {
467
        displayMessage(xi18nc("@info", "<title>Could not open file:</title><nl/>%1<br>Try adding a search path to the working directory in the Target Settings", filename), KTextEditor::Message::Error);
468 469 470
        return;
    }

Kåre Särs's avatar
Kåre Särs committed
471
    // open file (if needed, otherwise, this will activate only the right view...)
472
    m_win->openUrl(QUrl::fromLocalFile(filename));
Kåre Särs's avatar
Kåre Särs committed
473 474

    // do it ;)
475
    m_win->activeView()->setCursorPosition(KTextEditor::Cursor(line - 1, column - 1));
Kåre Särs's avatar
Kåre Särs committed
476 477 478
}

/******************************************************************/
479
void KateBuildView::addError(const QString &filename, const QString &line, const QString &column, const QString &message)
Kåre Särs's avatar
Kåre Särs committed
480
{
481
    ErrorCategory errorCategory = CategoryInfo;
482
    QTreeWidgetItem *item = new QTreeWidgetItem(m_buildUi.errTreeWidget);
Kåre Särs's avatar
Kåre Särs committed
483 484
    item->setBackground(1, Qt::gray);
    // The strings are twice in case kate is translated but not make.
485 486
    if (message.contains(QLatin1String("error")) || message.contains(i18nc("The same word as 'make' uses to mark an error.", "error")) || message.contains(QLatin1String("undefined reference")) ||
        message.contains(i18nc("The same word as 'ld' uses to mark an ...", "undefined reference"))) {
487
        errorCategory = CategoryError;
Kåre Särs's avatar
Kåre Särs committed
488
        item->setForeground(1, Qt::red);
Christoph Cullmann's avatar
mwolff:  
Christoph Cullmann committed
489
        m_numErrors++;
490
        item->setHidden(false);
Kåre Särs's avatar
Kåre Särs committed
491
    }
492
    if (message.contains(QLatin1String("warning")) || message.contains(i18nc("The same word as 'make' uses to mark a warning.", "warning"))) {
493
        errorCategory = CategoryWarning;
Kåre Särs's avatar
Kåre Särs committed
494
        item->setForeground(1, Qt::yellow);
Christoph Cullmann's avatar
mwolff:  
Christoph Cullmann committed
495
        m_numWarnings++;
496
        item->setHidden(m_buildUi.displayModeSlider->value() > 2);
Kåre Särs's avatar
Kåre Särs committed
497 498 499 500
    }
    item->setTextAlignment(1, Qt::AlignRight);

    // visible text
501
    // remove path from visible file name
502
    QFileInfo file(filename);
503

Kåre Särs's avatar
Kåre Särs committed
504 505 506 507 508 509 510 511 512
    item->setText(0, file.fileName());
    item->setText(1, line);
    item->setText(2, message.trimmed());

    // used to read from when activating an item
    item->setData(0, Qt::UserRole, filename);
    item->setData(1, Qt::UserRole, line);
    item->setData(2, Qt::UserRole, column);

513
    if (errorCategory == CategoryInfo) {
514
        item->setHidden(m_buildUi.displayModeSlider->value() > 1);
515
    }
516

517
    item->setData(0, ErrorRole, errorCategory);
518

Kåre Särs's avatar
Kåre Särs committed
519
    // add tooltips in all columns
520
    // The enclosing <qt>...</qt> enables word-wrap for long error messages
521
    item->setData(0, Qt::ToolTipRole, filename);
522 523
    item->setData(1, Qt::ToolTipRole, QStringLiteral("<qt>%1</qt>").arg(message));
    item->setData(2, Qt::ToolTipRole, QStringLiteral("<qt>%1</qt>").arg(message));
Kåre Särs's avatar
Kåre Särs committed
524 525
}

526 527
void KateBuildView::clearMarks()
{
528
    for (auto &doc : m_markedDocs) {
529 530 531 532
        if (!doc) {
            continue;
        }

533
        KTextEditor::MarkInterface *iface = qobject_cast<KTextEditor::MarkInterface *>(doc);
534
        if (iface) {
535 536
            const QHash<int, KTextEditor::Mark *> marks = iface->marks();
            QHashIterator<int, KTextEditor::Mark *> i(marks);
537 538 539 540 541 542 543 544 545 546 547 548 549
            while (i.hasNext()) {
                i.next();
                auto markType = KTextEditor::MarkInterface::Error | KTextEditor::MarkInterface::Warning;
                if (i.value()->type & markType) {
                    iface->removeMark(i.value()->line, markType);
                }
            }
        }
    }

    m_markedDocs.clear();
}

550
void KateBuildView::addMarks(KTextEditor::Document *doc, bool mark)
551
{
552 553 554
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0)
    KTextEditor::MarkInterfaceV2 *iface = qobject_cast<KTextEditor::MarkInterfaceV2 *>(doc);
#else
555
    KTextEditor::MarkInterface *iface = qobject_cast<KTextEditor::MarkInterface *>(doc);
556
#endif
557
    KTextEditor::MovingInterface *miface = qobject_cast<KTextEditor::MovingInterface *>(doc);
558 559 560 561 562 563
    if (!iface || m_markedDocs.contains(doc))
        return;

    QTreeWidgetItemIterator it(m_buildUi.errTreeWidget, QTreeWidgetItemIterator::All);
    while (*it) {
        QTreeWidgetItem *item = *it;
564
        ++it;
565 566 567 568 569 570 571

        auto filename = item->data(0, Qt::UserRole).toString();
        auto url = QUrl::fromLocalFile(filename);
        if (url != doc->url())
            continue;

        auto line = item->data(1, Qt::UserRole).toInt();
572
        if (mark) {
573
            ErrorCategory category = static_cast<ErrorCategory>(item->data(0, ErrorRole).toInt());
574 575 576
            KTextEditor::MarkInterface::MarkTypes markType {};

            switch (category) {
577 578 579 580 581 582 583 584 585 586 587 588
            case CategoryError: {
                markType = KTextEditor::MarkInterface::Error;
                iface->setMarkDescription(markType, i18n("Error"));
                break;
            }
            case CategoryWarning: {
                markType = KTextEditor::MarkInterface::Warning;
                iface->setMarkDescription(markType, i18n("Warning"));
                break;
            }
            default:
                break;
589
            }
590

591
            if (markType) {
592 593 594
#if KTEXTEDITOR_VERSION >= QT_VERSION_CHECK(5,69,0)
                iface->setMarkIcon(markType, messageIcon(category));
#else
595 596
                const int ps = 32;
                iface->setMarkPixmap(markType, messageIcon(category).pixmap(ps, ps));
597
#endif
598 599 600
                iface->addMark(line - 1, markType);
            }
            m_markedDocs.insert(doc, doc);
601 602
        }

603 604 605 606 607 608 609 610 611 612 613
        // add moving cursor so link between message and location
        // is not broken by document changes
        if (miface) {
            auto data = item->data(0, DataRole).value<ItemData>();
            if (!data.cursor) {
                auto column = item->data(2, Qt::UserRole).toInt();
                data.cursor.reset(miface->newMovingCursor({line, column}));
                QVariant var;
                var.setValue(data);
                item->setData(0, DataRole, var);
            }
614 615 616
        }
    }

617 618
    // ensure cleanup
    if (miface) {
619 620
        auto conn = connect(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(slotInvalidateMoving(KTextEditor::Document *)), Qt::UniqueConnection);
        conn = connect(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *)), this, SLOT(slotInvalidateMoving(KTextEditor::Document *)), Qt::UniqueConnection);
621 622
    }

623
    connect(doc, SIGNAL(markClicked(KTextEditor::Document *, KTextEditor::Mark, bool &)), this, SLOT(slotMarkClicked(KTextEditor::Document *, KTextEditor::Mark, bool &)), Qt::UniqueConnection);
624
}
625

626
void KateBuildView::slotInvalidateMoving(KTextEditor::Document *doc)
627 628 629 630
{
    QTreeWidgetItemIterator it(m_buildUi.errTreeWidget, QTreeWidgetItemIterator::All);
    while (*it) {
        QTreeWidgetItem *item = *it;
631
        ++it;
632 633 634 635 636 637

        auto data = item->data(0, DataRole).value<ItemData>();
        if (data.cursor && data.cursor->document() == doc) {
            item->setData(0, DataRole, 0);
        }
    }
638 639 640 641 642 643 644 645
}

void KateBuildView::slotMarkClicked(KTextEditor::Document *doc, KTextEditor::Mark mark, bool &handled)
{
    auto tree = m_buildUi.errTreeWidget;
    QTreeWidgetItemIterator it(tree, QTreeWidgetItemIterator::All);
    while (*it) {
        QTreeWidgetItem *item = *it;
646
        ++it;
647 648 649

        auto filename = item->data(0, Qt::UserRole).toString();
        auto line = item->data(1, Qt::UserRole).toInt();
650 651 652 653 654
        // prefer moving cursor's opinion if so available
        auto data = item->data(0, DataRole).value<ItemData>();
        if (data.cursor) {
            line = data.cursor->line();
        }
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
        if (line - 1 == mark.line && QUrl::fromLocalFile(filename) == doc->url()) {
            tree->blockSignals(true);
            tree->setCurrentItem(item);
            tree->scrollToItem(item, QAbstractItemView::PositionAtCenter);
            tree->blockSignals(false);
            handled = true;
            break;
        }
    }
}

void KateBuildView::slotViewChanged()
{
    KTextEditor::View *activeView = m_win->activeView();
    auto doc = activeView ? activeView->document() : nullptr;

671 672
    if (doc) {
        addMarks(doc, m_showMarks->isChecked());
673 674 675 676 677 678 679 680 681 682 683 684 685 686
    }
}

void KateBuildView::slotDisplayOption()
{
    if (m_showMarks) {
        if (!m_showMarks->isChecked()) {
            clearMarks();
        } else {
            slotViewChanged();
        }
    }
}

687
/******************************************************************/
688
QUrl KateBuildView::docUrl()
689
{
690
    KTextEditor::View *kv = m_win->activeView();
691
    if (!kv) {
692
        qDebug() << "no KTextEditor::View";
693
        return QUrl();
694
    }
695

696 697
    if (kv->document()->isModified())
        kv->document()->save();
698 699 700 701
    return kv->document()->url();
}

/******************************************************************/
702
bool KateBuildView::checkLocal(const QUrl &dir)
703 704
{
    if (dir.path().isEmpty()) {
705
        KMessageBox::sorry(nullptr, i18n("There is no file or directory specified for building."));
706
        return false;
707 708 709 710 711
    } else if (!dir.isLocalFile()) {
        KMessageBox::sorry(nullptr,
                           i18n("The file \"%1\" is not a local file. "
                                "Non-local files cannot be compiled.",
                                dir.path()));
712 713 714 715 716
        return false;
    }
    return true;
}

Kåre Särs's avatar
Kåre Särs committed
717
/******************************************************************/
718
void KateBuildView::clearBuildResults()
Kåre Särs's avatar
Kåre Särs committed
719
{
720
    clearMarks();
721 722
    m_buildUi.plainTextEdit->clear();
    m_buildUi.errTreeWidget->clear();
723 724
    m_stdOut.clear();
    m_stdErr.clear();
Christoph Cullmann's avatar
mwolff:  
Christoph Cullmann committed
725 726
    m_numErrors = 0;
    m_numWarnings = 0;
Kåre Särs's avatar
Kåre Särs committed
727
    m_make_dir_stack.clear();
728 729
}

730
/******************************************************************/
731 732
bool KateBuildView::startProcess(const QString &dir, const QString &command)
{
733
    if (m_proc.state() != QProcess::NotRunning) {
734 735 736 737 738
        return false;
    }

    // clear previous runs
    clearBuildResults();
739

Kåre Särs's avatar
Kåre Särs committed
740
    // activate the output tab
741
    m_buildUi.u_tabWidget->setCurrentIndex(1);
742 743
    m_displayModeBeforeBuild = m_buildUi.displayModeSlider->value();
    m_buildUi.displayModeSlider->setValue(0);
744
    m_win->showToolView(m_toolView);
Kåre Särs's avatar
Kåre Särs committed
745 746

    // set working directory
747 748
    m_make_dir = dir;
    m_make_dir_stack.push(m_make_dir);
749 750 751 752 753 754

    if (!QFile::exists(m_make_dir)) {
        KMessageBox::error(nullptr, i18n("Cannot run command: %1\nWork path does not exist: %2", command, m_make_dir));
        return false;
    }

755 756 757 758 759 760 761 762 763 764
    // ninja build tool sends all output to stdout,
    // so follow https://github.com/ninja-build/ninja/issues/1537 to separate ninja and compiler output
    auto env = QProcessEnvironment::systemEnvironment();
    const auto nstatus = QStringLiteral("NINJA_STATUS");
    auto curr = env.value(nstatus, QStringLiteral("[%f/%t] "));
    // add marker to search on later on
    env.insert(nstatus, NinjaPrefix + curr);
    m_ninjaBuildDetected = false;

    m_proc.setProcessEnvironment(env);
765 766 767
    m_proc.setWorkingDirectory(m_make_dir);
    m_proc.setShellCommand(command);
    m_proc.start();
Kåre Särs's avatar
Kåre Särs committed
768

769
    if (!m_proc.waitForStarted(500)) {
770
        KMessageBox::error(nullptr, i18n("Failed to run \"%1\". exitStatus = %2", command, m_proc.exitStatus()));
Kåre Särs's avatar
Kåre Särs committed
771 772 773
        return false;
    }

774 775 776 777 778
    m_buildUi.cancelBuildButton->setEnabled(true);
    m_buildUi.cancelBuildButton2->setEnabled(true);
    m_buildUi.buildAgainButton->setEnabled(false);
    m_buildUi.buildAgainButton2->setEnabled(false);

779
    QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
Kåre Särs's avatar
Kåre Särs committed
780 781 782 783 784 785
    return true;
}

/******************************************************************/
bool KateBuildView::slotStop()
{
786
    if (m_proc.state() != QProcess::NotRunning) {
787
        m_buildCancelled = true;
Kåre Särs's avatar
Kåre Särs committed
788
        QString msg = i18n("Building <b>%1</b> cancelled", m_currentlyBuildingTarget);
789 790
        m_buildUi.buildStatusLabel->setText(msg);
        m_buildUi.buildStatusLabel2->setText(msg);
791
        m_proc.terminate();
Kåre Särs's avatar
Kåre Särs committed
792 793 794 795 796
        return true;
    }
    return false;
}

797
/******************************************************************/
798 799
void KateBuildView::slotBuildActiveTarget()
{
800 801
    if (!m_targetsUi->targetsView->currentIndex().isValid()) {
        slotSelectTarget();
802
    } else {
803 804 805
        buildCurrentTarget();
    }
}
806

807
/******************************************************************/
808 809
void KateBuildView::slotBuildPreviousTarget()
{
810 811
    if (!m_previousIndex.isValid()) {
        slotSelectTarget();
812
    } else {
813 814
        m_targetsUi->targetsView->setCurrentIndex(m_previousIndex);
        buildCurrentTarget();
815 816 817 818
    }
}

/******************************************************************/
819 820
void KateBuildView::slotBuildDefaultTarget()
{
821
    QModelIndex defaultTarget = m_targetsUi->targetsModel.defaultTarget(m_targetsUi->targetCombo->currentIndex());
822 823 824 825 826
    m_targetsUi->targetsView->setCurrentIndex(defaultTarget);
    buildCurrentTarget();
}

/******************************************************************/
827 828
void KateBuildView::slotSelectTarget()
{
829 830 831 832 833 834 835 836 837 838
    SelectTargetView *dialog = new SelectTargetView(&(m_targetsUi->targetsModel));

    dialog->setCurrentIndex(m_targetsUi->targetsView->currentIndex());

    int result = dialog->exec();
    if (result == QDialog::Accepted) {
        m_targetsUi->targetsView->setCurrentIndex(dialog->currentIndex());
        buildCurrentTarget();
    }
    delete dialog;
839
    dialog = nullptr;
840 841 842
}

/******************************************************************/
843
bool KateBuildView::buildCurrentTarget()
844
{
845
    if (m_proc.state() != QProcess::NotRunning) {
846 847 848 849