1 /* plugin_katebuild.c                    Kate Plugin
2 **
3 ** SPDX-FileCopyrightText: 2013 Alexander Neundorf <neundorf@kde.org>
4 ** SPDX-FileCopyrightText: 2006-2015 Kåre Särs <kare.sars@iki.fi>
5 ** SPDX-FileCopyrightText: 2011 Ian Wakeling <ian.wakeling@ntlworld.com>
6 **
7 ** This code is mostly a modification of the GPL'ed Make plugin
8 ** by Adriaan de Groot.
9 */
10 
11 /*
12 ** SPDX-License-Identifier: GPL-2.0-or-later
13 **
14 ** This program is distributed in the hope that it will be useful,
15 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ** GNU General Public License for more details.
18 **
19 ** You should have received a copy of the GNU General Public License
20 ** along with this program in a file called COPYING; if not, write to
21 ** the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 ** MA 02110-1301, USA.
23 */
24 
25 #include "plugin_katebuild.h"
26 
27 #include <cassert>
28 
29 #include <QCompleter>
30 #include <QDir>
31 #include <QDirModel>
32 #include <QFileDialog>
33 #include <QFileInfo>
34 #include <QIcon>
35 #include <QKeyEvent>
36 #include <QRegularExpressionMatch>
37 #include <QScrollBar>
38 #include <QString>
39 
40 #include <QAction>
41 
42 #include <KActionCollection>
43 #include <KTextEditor/Application>
44 #include <KTextEditor/ConfigInterface>
45 #include <KTextEditor/Editor>
46 #include <KTextEditor/MarkInterface>
47 #include <KTextEditor/MovingInterface>
48 
49 #include <KAboutData>
50 #include <KLocalizedString>
51 #include <KMessageBox>
52 #include <KPluginFactory>
53 #include <KXMLGUIFactory>
54 
55 #include "SelectTargetView.h"
56 
57 K_PLUGIN_FACTORY_WITH_JSON(KateBuildPluginFactory, "katebuildplugin.json", registerPlugin<KateBuildPlugin>();)
58 
59 static const QString DefConfigCmd = QStringLiteral("cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_EXPORT_COMPILE_COMMANDS=1 ../");
60 static const QString DefConfClean;
61 static const QString DefTargetName = QStringLiteral("all");
62 static const QString DefBuildCmd = QStringLiteral("make");
63 static const QString DefCleanCmd = QStringLiteral("make clean");
64 static const QString NinjaPrefix = QStringLiteral("[ninja]");
65 
messageIcon(KateBuildView::ErrorCategory severity)66 static QIcon messageIcon(KateBuildView::ErrorCategory severity)
67 {
68     // clang-format off
69 #define RETURN_CACHED_ICON(name, fallbackname) \
70     { \
71         static QIcon icon(QIcon::fromTheme(QStringLiteral(name), \
72                                            QIcon::fromTheme(QStringLiteral(fallbackname)))); \
73         return icon; \
74     }
75     // clang-format on
76     switch (severity) {
77     case KateBuildView::CategoryError:
78         RETURN_CACHED_ICON("data-error", "dialog-error")
79     case KateBuildView::CategoryWarning:
80         RETURN_CACHED_ICON("data-warning", "dialog-warning")
81     default:
82         break;
83     }
84     return QIcon();
85 }
86 
87 struct ItemData {
88     // ensure destruction, but not inadvertently so by a variant value copy
89     QSharedPointer<KTextEditor::MovingCursor> cursor;
90 };
91 
Q_DECLARE_METATYPE(ItemData)92 Q_DECLARE_METATYPE(ItemData)
93 
94 /******************************************************************/
95 KateBuildPlugin::KateBuildPlugin(QObject *parent, const VariantList &)
96     : KTextEditor::Plugin(parent)
97 {
98     // KF5 FIXME KGlobal::locale()->insertCatalog("katebuild-plugin");
99 }
100 
101 /******************************************************************/
createView(KTextEditor::MainWindow * mainWindow)102 QObject *KateBuildPlugin::createView(KTextEditor::MainWindow *mainWindow)
103 {
104     return new KateBuildView(this, mainWindow);
105 }
106 
107 /******************************************************************/
KateBuildView(KTextEditor::Plugin * plugin,KTextEditor::MainWindow * mw)108 KateBuildView::KateBuildView(KTextEditor::Plugin *plugin, KTextEditor::MainWindow *mw)
109     : QObject(mw)
110     , m_win(mw)
111     , m_buildWidget(nullptr)
112     , m_outputWidgetWidth(0)
113     , m_proc(this)
114     , m_stdOut()
115     , m_stdErr()
116     , m_buildCancelled(false)
117     , m_displayModeBeforeBuild(1)
118     // NOTE this will not allow spaces in file names.
119     // e.g. from gcc: "main.cpp:14: error: cannot convert ‘std::string’ to ‘int’ in return"
120     // e.g. from gcc: "main.cpp:14:8: error: cannot convert ‘std::string’ to ‘int’ in return"
121     // e.g. from icpc: "main.cpp(14): error: no suitable conversion function from "std::string" to "int" exists"
122     // e.g. from clang: ""main.cpp(14,8): fatal error: 'boost/scoped_array.hpp' file not found"
123     , m_filenameDetector(QStringLiteral("((?:[a-np-zA-Z]:[\\\\/])?[^\\s:(]+)[:\\(](\\d+)[,:]?(\\d+)?[\\):]* (.*)"))
124     , m_newDirDetector(QStringLiteral("make\\[.+\\]: .+ '(.*)'"))
125 {
126     KXMLGUIClient::setComponentName(QStringLiteral("katebuild"), i18n("Kate Build Plugin"));
127     setXMLFile(QStringLiteral("ui.rc"));
128 
129     m_toolView = mw->createToolView(plugin,
130                                     QStringLiteral("kate_plugin_katebuildplugin"),
131                                     KTextEditor::MainWindow::Bottom,
132                                     QIcon::fromTheme(QStringLiteral("application-x-ms-dos-executable")),
133                                     i18n("Build Output"));
134 
135     QAction *a = actionCollection()->addAction(QStringLiteral("select_target"));
136     a->setText(i18n("Select Target..."));
137     a->setIcon(QIcon::fromTheme(QStringLiteral("select")));
138     connect(a, &QAction::triggered, this, &KateBuildView::slotSelectTarget);
139     a = actionCollection()->addAction(QStringLiteral("build_default_target"));
140     a->setText(i18n("Build Default Target"));
141     connect(a, &QAction::triggered, this, &KateBuildView::slotBuildDefaultTarget);
142 
143     a = actionCollection()->addAction(QStringLiteral("build_previous_target"));
144     a->setText(i18n("Build Previous Target"));
145     connect(a, &QAction::triggered, this, &KateBuildView::slotBuildPreviousTarget);
146 
147     a = actionCollection()->addAction(QStringLiteral("stop"));
148     a->setText(i18n("Stop"));
149     a->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
150     connect(a, &QAction::triggered, this, &KateBuildView::slotStop);
151 
152     a = actionCollection()->addAction(QStringLiteral("goto_next"));
153     a->setText(i18n("Next Error"));
154     a->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
155     actionCollection()->setDefaultShortcut(a, Qt::SHIFT | Qt::ALT | Qt::Key_Right);
156     connect(a, &QAction::triggered, this, &KateBuildView::slotNext);
157 
158     a = actionCollection()->addAction(QStringLiteral("goto_prev"));
159     a->setText(i18n("Previous Error"));
160     a->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
161     actionCollection()->setDefaultShortcut(a, Qt::SHIFT | Qt::ALT | Qt::Key_Left);
162     connect(a, &QAction::triggered, this, &KateBuildView::slotPrev);
163 
164     m_showMarks = a = actionCollection()->addAction(QStringLiteral("show_marks"));
165     a->setText(i18n("Show Marks"));
166     a->setCheckable(true);
167     connect(a, &QAction::triggered, this, &KateBuildView::slotDisplayOption);
168 
169     m_buildWidget = new QWidget(m_toolView);
170     m_buildUi.setupUi(m_buildWidget);
171     m_targetsUi = new TargetsUi(this, m_buildUi.u_tabWidget);
172     m_buildUi.u_tabWidget->insertTab(0, m_targetsUi, i18nc("Tab label", "Target Settings"));
173     m_buildUi.u_tabWidget->setCurrentWidget(m_targetsUi);
174 
175     m_buildWidget->installEventFilter(this);
176 
177     m_buildUi.buildAgainButton->setVisible(true);
178     m_buildUi.cancelBuildButton->setVisible(true);
179     m_buildUi.buildStatusLabel->setVisible(true);
180     m_buildUi.buildAgainButton2->setVisible(false);
181     m_buildUi.cancelBuildButton2->setVisible(false);
182     m_buildUi.buildStatusLabel2->setVisible(false);
183     m_buildUi.extraLineLayout->setAlignment(Qt::AlignRight);
184     m_buildUi.cancelBuildButton->setEnabled(false);
185     m_buildUi.cancelBuildButton2->setEnabled(false);
186 
187     connect(m_buildUi.errTreeWidget, &QTreeWidget::itemClicked, this, &KateBuildView::slotErrorSelected);
188 
189     m_buildUi.plainTextEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
190     m_buildUi.plainTextEdit->setReadOnly(true);
191     slotDisplayMode(FullOutput);
192 
193     auto updateEditorColors = [this](KTextEditor::Editor *e) {
194         if (!e)
195             return;
196         auto theme = e->theme();
197         auto bg = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::BackgroundColor));
198         auto fg = QColor::fromRgba(theme.textColor(KSyntaxHighlighting::Theme::TextStyle::Normal));
199         auto sel = QColor::fromRgba(theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::TextSelection));
200         auto pal = m_buildUi.plainTextEdit->palette();
201         pal.setColor(QPalette::Base, bg);
202         pal.setColor(QPalette::Text, fg);
203         pal.setColor(QPalette::Highlight, sel);
204         pal.setColor(QPalette::HighlightedText, fg);
205         m_buildUi.plainTextEdit->setPalette(pal);
206     };
207     connect(KTextEditor::Editor::instance(), &KTextEditor::Editor::configChanged, this, updateEditorColors);
208 
209     connect(m_buildUi.displayModeSlider, &QSlider::valueChanged, this, &KateBuildView::slotDisplayMode);
210 
211     connect(m_buildUi.buildAgainButton, &QPushButton::clicked, this, &KateBuildView::slotBuildPreviousTarget);
212     connect(m_buildUi.cancelBuildButton, &QPushButton::clicked, this, &KateBuildView::slotStop);
213     connect(m_buildUi.buildAgainButton2, &QPushButton::clicked, this, &KateBuildView::slotBuildPreviousTarget);
214     connect(m_buildUi.cancelBuildButton2, &QPushButton::clicked, this, &KateBuildView::slotStop);
215 
216     connect(m_targetsUi->newTarget, &QToolButton::clicked, this, &KateBuildView::targetSetNew);
217     connect(m_targetsUi->copyTarget, &QToolButton::clicked, this, &KateBuildView::targetOrSetCopy);
218     connect(m_targetsUi->deleteTarget, &QToolButton::clicked, this, &KateBuildView::targetDelete);
219 
220     connect(m_targetsUi->addButton, &QToolButton::clicked, this, &KateBuildView::slotAddTargetClicked);
221     connect(m_targetsUi->buildButton, &QToolButton::clicked, this, &KateBuildView::slotBuildActiveTarget);
222     connect(m_targetsUi, &TargetsUi::enterPressed, this, &KateBuildView::slotBuildActiveTarget);
223 
224     m_proc.setOutputChannelMode(KProcess::SeparateChannels);
225     connect(&m_proc, static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &KateBuildView::slotProcExited);
226     connect(&m_proc, &KProcess::readyReadStandardError, this, &KateBuildView::slotReadReadyStdErr);
227     connect(&m_proc, &KProcess::readyReadStandardOutput, this, &KateBuildView::slotReadReadyStdOut);
228 
229     connect(m_win, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KateBuildView::handleEsc);
230     connect(m_win, &KTextEditor::MainWindow::viewChanged, this, &KateBuildView::slotViewChanged);
231 
232     m_toolView->installEventFilter(this);
233 
234     m_win->guiFactory()->addClient(this);
235 
236     // watch for project plugin view creation/deletion
237     connect(m_win, &KTextEditor::MainWindow::pluginViewCreated, this, &KateBuildView::slotPluginViewCreated);
238     connect(m_win, &KTextEditor::MainWindow::pluginViewDeleted, this, &KateBuildView::slotPluginViewDeleted);
239 
240     // Connect signals from project plugin to our slots
241     m_projectPluginView = m_win->pluginView(QStringLiteral("kateprojectplugin"));
242     slotPluginViewCreated(QStringLiteral("kateprojectplugin"), m_projectPluginView);
243 }
244 
245 /******************************************************************/
~KateBuildView()246 KateBuildView::~KateBuildView()
247 {
248     m_win->guiFactory()->removeClient(this);
249     delete m_toolView;
250 }
251 
252 /******************************************************************/
readSessionConfig(const KConfigGroup & cg)253 void KateBuildView::readSessionConfig(const KConfigGroup &cg)
254 {
255     int numTargets = cg.readEntry(QStringLiteral("NumTargets"), 0);
256     m_targetsUi->targetsModel.clear();
257     int tmpIndex;
258     int tmpCmd;
259 
260     if (numTargets == 0) {
261         // either the config is empty or uses the older format
262         m_targetsUi->targetsModel.addTargetSet(i18n("Target Set"), QString());
263         m_targetsUi->targetsModel.addCommand(0, i18n("build"), cg.readEntry(QStringLiteral("Make Command"), DefBuildCmd));
264         m_targetsUi->targetsModel.addCommand(0, i18n("clean"), cg.readEntry(QStringLiteral("Clean Command"), DefCleanCmd));
265         m_targetsUi->targetsModel.addCommand(0, i18n("config"), DefConfigCmd);
266 
267         QString quickCmd = cg.readEntry(QStringLiteral("Quick Compile Command"));
268         if (!quickCmd.isEmpty()) {
269             m_targetsUi->targetsModel.addCommand(0, i18n("quick"), quickCmd);
270         }
271         tmpIndex = 0;
272         tmpCmd = 0;
273     } else {
274         for (int i = 0; i < numTargets; i++) {
275             QStringList targetNames = cg.readEntry(QStringLiteral("%1 Target Names").arg(i), QStringList());
276             QString targetSetName = cg.readEntry(QStringLiteral("%1 Target").arg(i), QString());
277             QString buildDir = cg.readEntry(QStringLiteral("%1 BuildPath").arg(i), QString());
278 
279             m_targetsUi->targetsModel.addTargetSet(targetSetName, buildDir);
280 
281             if (targetNames.isEmpty()) {
282                 QString quickCmd = cg.readEntry(QStringLiteral("%1 QuickCmd").arg(i));
283                 m_targetsUi->targetsModel.addCommand(i, i18n("build"), cg.readEntry(QStringLiteral("%1 BuildCmd"), DefBuildCmd));
284                 m_targetsUi->targetsModel.addCommand(i, i18n("clean"), cg.readEntry(QStringLiteral("%1 CleanCmd"), DefCleanCmd));
285                 if (!quickCmd.isEmpty()) {
286                     m_targetsUi->targetsModel.addCommand(i, i18n("quick"), quickCmd);
287                 }
288                 m_targetsUi->targetsModel.setDefaultCmd(i, i18n("build"));
289             } else {
290                 for (int tn = 0; tn < targetNames.size(); ++tn) {
291                     const QString &targetName = targetNames.at(tn);
292                     m_targetsUi->targetsModel.addCommand(i, targetName, cg.readEntry(QStringLiteral("%1 BuildCmd %2").arg(i).arg(targetName), DefBuildCmd));
293                 }
294                 QString defCmd = cg.readEntry(QStringLiteral("%1 Target Default").arg(i), QString());
295                 m_targetsUi->targetsModel.setDefaultCmd(i, defCmd);
296             }
297         }
298         tmpIndex = cg.readEntry(QStringLiteral("Active Target Index"), 0);
299         tmpCmd = cg.readEntry(QStringLiteral("Active Target Command"), 0);
300     }
301 
302     m_targetsUi->targetsView->expandAll();
303     m_targetsUi->targetsView->resizeColumnToContents(0);
304     m_targetsUi->targetsView->collapseAll();
305 
306     QModelIndex root = m_targetsUi->targetsModel.index(tmpIndex);
307     QModelIndex cmdIndex = m_targetsUi->targetsModel.index(tmpCmd, 0, root);
308     m_targetsUi->targetsView->setCurrentIndex(cmdIndex);
309 
310     auto showMarks = cg.readEntry(QStringLiteral("Show Marks"), false);
311     m_showMarks->setChecked(showMarks);
312 
313     // Add project targets, if any
314     slotAddProjectTarget();
315 }
316 
317 /******************************************************************/
writeSessionConfig(KConfigGroup & cg)318 void KateBuildView::writeSessionConfig(KConfigGroup &cg)
319 {
320     // Don't save project targets, is not our area of accountability
321     m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets"));
322 
323     QList<TargetModel::TargetSet> targets = m_targetsUi->targetsModel.targetSets();
324 
325     cg.writeEntry("NumTargets", targets.size());
326 
327     for (int i = 0; i < targets.size(); i++) {
328         cg.writeEntry(QStringLiteral("%1 Target").arg(i), targets[i].name);
329         cg.writeEntry(QStringLiteral("%1 BuildPath").arg(i), targets[i].workDir);
330         QStringList cmdNames;
331 
332         for (int j = 0; j < targets[i].commands.count(); j++) {
333             const QString &cmdName = targets[i].commands[j].first;
334             const QString &buildCmd = targets[i].commands[j].second;
335             cmdNames << cmdName;
336             cg.writeEntry(QStringLiteral("%1 BuildCmd %2").arg(i).arg(cmdName), buildCmd);
337         }
338         cg.writeEntry(QStringLiteral("%1 Target Names").arg(i), cmdNames);
339         cg.writeEntry(QStringLiteral("%1 Target Default").arg(i), targets[i].defaultCmd);
340     }
341     int setRow = 0;
342     int set = 0;
343     QModelIndex ind = m_targetsUi->targetsView->currentIndex();
344     if (ind.internalId() == TargetModel::InvalidIndex) {
345         set = ind.row();
346     } else {
347         set = ind.internalId();
348         setRow = ind.row();
349     }
350     if (setRow < 0) {
351         setRow = 0;
352     }
353 
354     cg.writeEntry(QStringLiteral("Active Target Index"), set);
355     cg.writeEntry(QStringLiteral("Active Target Command"), setRow);
356     cg.writeEntry(QStringLiteral("Show Marks"), m_showMarks->isChecked());
357 
358     // Restore project targets, if any
359     slotAddProjectTarget();
360 }
361 
362 /******************************************************************/
slotNext()363 void KateBuildView::slotNext()
364 {
365     const int itemCount = m_buildUi.errTreeWidget->topLevelItemCount();
366     if (itemCount == 0) {
367         return;
368     }
369 
370     QTreeWidgetItem *item = m_buildUi.errTreeWidget->currentItem();
371     if (item && item->isHidden()) {
372         item = nullptr;
373     }
374 
375     int i = (item == nullptr) ? -1 : m_buildUi.errTreeWidget->indexOfTopLevelItem(item);
376 
377     while (++i < itemCount) {
378         item = m_buildUi.errTreeWidget->topLevelItem(i);
379         // Search item which fit view settings and has desired data
380         if (!item->text(1).isEmpty() && !item->isHidden() && item->data(1, Qt::UserRole).toInt()) {
381             m_buildUi.errTreeWidget->setCurrentItem(item);
382             m_buildUi.errTreeWidget->scrollToItem(item);
383             slotErrorSelected(item);
384             return;
385         }
386     }
387 }
388 
389 /******************************************************************/
slotPrev()390 void KateBuildView::slotPrev()
391 {
392     const int itemCount = m_buildUi.errTreeWidget->topLevelItemCount();
393     if (itemCount == 0) {
394         return;
395     }
396 
397     QTreeWidgetItem *item = m_buildUi.errTreeWidget->currentItem();
398     if (item && item->isHidden()) {
399         item = nullptr;
400     }
401 
402     int i = (item == nullptr) ? itemCount : m_buildUi.errTreeWidget->indexOfTopLevelItem(item);
403 
404     while (--i >= 0) {
405         item = m_buildUi.errTreeWidget->topLevelItem(i);
406         // Search item which fit view settings and has desired data
407         if (!item->text(1).isEmpty() && !item->isHidden() && item->data(1, Qt::UserRole).toInt()) {
408             m_buildUi.errTreeWidget->setCurrentItem(item);
409             m_buildUi.errTreeWidget->scrollToItem(item);
410             slotErrorSelected(item);
411             return;
412         }
413     }
414 }
415 
416 #ifdef Q_OS_WIN
417 /******************************************************************/
caseFixed(const QString & path)418 QString KateBuildView::caseFixed(const QString &path)
419 {
420     QStringList paths = path.split(QLatin1Char('/'));
421     if (paths.isEmpty()) {
422         return path;
423     }
424 
425     QString result = paths[0].toUpper() + QLatin1Char('/');
426     for (int i = 1; i < paths.count(); ++i) {
427         QDir curDir(result);
428         const QStringList items = curDir.entryList();
429         int j;
430         for (j = 0; j < items.size(); ++j) {
431             if (items[j].compare(paths[i], Qt::CaseInsensitive) == 0) {
432                 result += items[j];
433                 if (i < paths.count() - 1) {
434                     result += QLatin1Char('/');
435                 }
436                 break;
437             }
438         }
439         if (j == items.size()) {
440             return path;
441         }
442     }
443     return result;
444 }
445 #endif
446 
slotErrorSelected(QTreeWidgetItem * item)447 void KateBuildView::slotErrorSelected(QTreeWidgetItem *item)
448 {
449     // any view active?
450     if (!m_win->activeView()) {
451         return;
452     }
453 
454     // Avoid garish highlighting of the selected line
455     m_win->activeView()->setFocus();
456 
457     // Search the item where the data we need is stored
458     while (!item->data(1, Qt::UserRole).toInt()) {
459         item = m_buildUi.errTreeWidget->itemAbove(item);
460         if (!item) {
461             return;
462         }
463     }
464 
465     // get stuff
466     QString filename = item->data(0, Qt::UserRole).toString();
467     if (filename.isEmpty()) {
468         return;
469     }
470 
471     int line = item->data(1, Qt::UserRole).toInt();
472     int column = item->data(2, Qt::UserRole).toInt();
473     // check with moving cursor
474     auto data = item->data(0, DataRole).value<ItemData>();
475     if (data.cursor) {
476         line = data.cursor->line();
477         column = data.cursor->column();
478     }
479 
480 #ifdef Q_OS_WIN
481     filename = caseFixed(filename);
482 #endif
483 
484     // Check if the file exists
485     if (!QFileInfo::exists(filename)) {
486         displayMessage(xi18nc("@info",
487                               "<title>Could not open file:</title><nl/>%1<br/>Try adding a search path to the working directory in the Target Settings",
488                               filename),
489                        KTextEditor::Message::Error);
490         return;
491     }
492 
493     // open file (if needed, otherwise, this will activate only the right view...)
494     m_win->openUrl(QUrl::fromLocalFile(filename));
495 
496     // do it ;)
497     m_win->activeView()->setCursorPosition(KTextEditor::Cursor(line - 1, column - 1));
498 }
499 
500 /******************************************************************/
addError(const QString & filename,const QString & line,const QString & column,const QString & message)501 void KateBuildView::addError(const QString &filename, const QString &line, const QString &column, const QString &message)
502 {
503     ErrorCategory errorCategory = CategoryInfo;
504     QTreeWidgetItem *item = new QTreeWidgetItem(m_buildUi.errTreeWidget);
505     item->setBackground(1, Qt::gray);
506     // The strings are twice in case kate is translated but not make.
507     if (message.contains(QLatin1String("error")) || message.contains(i18nc("The same word as 'make' uses to mark an error.", "error"))
508         || message.contains(QLatin1String("undefined reference"))
509         || message.contains(i18nc("The same word as 'ld' uses to mark an ...", "undefined reference"))) {
510         errorCategory = CategoryError;
511         item->setForeground(1, Qt::red);
512         m_numErrors++;
513         item->setHidden(false);
514     }
515     if (message.contains(QLatin1String("warning")) || message.contains(i18nc("The same word as 'make' uses to mark a warning.", "warning"))) {
516         errorCategory = CategoryWarning;
517         item->setForeground(1, Qt::yellow);
518         m_numWarnings++;
519         item->setHidden(m_buildUi.displayModeSlider->value() > 2);
520     }
521     item->setTextAlignment(1, Qt::AlignRight);
522 
523     // visible text
524     // remove path from visible file name
525     QFileInfo file(filename);
526 
527     item->setText(0, file.fileName());
528     item->setText(1, line);
529     item->setText(2, message);
530 
531     // used to read from when activating an item
532     item->setData(0, Qt::UserRole, filename);
533     item->setData(1, Qt::UserRole, line);
534     item->setData(2, Qt::UserRole, column);
535 
536     if (errorCategory == CategoryInfo) {
537         item->setHidden(m_buildUi.displayModeSlider->value() > 1);
538     }
539 
540     item->setData(0, ErrorRole, errorCategory);
541 
542     // add tooltips in all columns
543     // The enclosing <qt>...</qt> enables word-wrap for long error messages
544     item->setData(0, Qt::ToolTipRole, filename);
545     item->setData(1, Qt::ToolTipRole, QStringLiteral("<qt>%1</qt>").arg(message));
546     item->setData(2, Qt::ToolTipRole, QStringLiteral("<qt>%1</qt>").arg(message));
547 }
548 
clearMarks()549 void KateBuildView::clearMarks()
550 {
551     for (auto &doc : m_markedDocs) {
552         if (!doc) {
553             continue;
554         }
555 
556         KTextEditor::MarkInterface *iface = qobject_cast<KTextEditor::MarkInterface *>(doc);
557         if (iface) {
558             const QHash<int, KTextEditor::Mark *> marks = iface->marks();
559             QHashIterator<int, KTextEditor::Mark *> i(marks);
560             while (i.hasNext()) {
561                 i.next();
562                 auto markType = KTextEditor::MarkInterface::Error | KTextEditor::MarkInterface::Warning;
563                 if (i.value()->type & markType) {
564                     iface->removeMark(i.value()->line, markType);
565                 }
566             }
567         }
568     }
569 
570     m_markedDocs.clear();
571 }
572 
addMarks(KTextEditor::Document * doc,bool mark)573 void KateBuildView::addMarks(KTextEditor::Document *doc, bool mark)
574 {
575     KTextEditor::MarkInterfaceV2 *iface = qobject_cast<KTextEditor::MarkInterfaceV2 *>(doc);
576     KTextEditor::MovingInterface *miface = qobject_cast<KTextEditor::MovingInterface *>(doc);
577     if (!iface || m_markedDocs.contains(doc)) {
578         return;
579     }
580 
581     QTreeWidgetItemIterator it(m_buildUi.errTreeWidget, QTreeWidgetItemIterator::All);
582     while (*it) {
583         QTreeWidgetItem *item = *it;
584         ++it;
585 
586         auto filename = item->data(0, Qt::UserRole).toString();
587         auto url = QUrl::fromLocalFile(filename);
588         if (url != doc->url()) {
589             continue;
590         }
591 
592         auto line = item->data(1, Qt::UserRole).toInt();
593         if (mark) {
594             ErrorCategory category = static_cast<ErrorCategory>(item->data(0, ErrorRole).toInt());
595             KTextEditor::MarkInterface::MarkTypes markType{};
596 
597             switch (category) {
598             case CategoryError: {
599                 markType = KTextEditor::MarkInterface::Error;
600                 iface->setMarkDescription(markType, i18n("Error"));
601                 break;
602             }
603             case CategoryWarning: {
604                 markType = KTextEditor::MarkInterface::Warning;
605                 iface->setMarkDescription(markType, i18n("Warning"));
606                 break;
607             }
608             default:
609                 break;
610             }
611 
612             if (markType) {
613                 iface->setMarkIcon(markType, messageIcon(category));
614                 iface->addMark(line - 1, markType);
615             }
616             m_markedDocs.insert(doc, doc);
617         }
618 
619         // add moving cursor so link between message and location
620         // is not broken by document changes
621         if (miface) {
622             auto data = item->data(0, DataRole).value<ItemData>();
623             if (!data.cursor) {
624                 auto column = item->data(2, Qt::UserRole).toInt();
625                 data.cursor.reset(miface->newMovingCursor({line, column}));
626                 QVariant var;
627                 var.setValue(data);
628                 item->setData(0, DataRole, var);
629             }
630         }
631     }
632 
633     // ensure cleanup
634     // clang-format off
635     if (miface) {
636         auto conn = connect(doc,
637                             SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document*)),
638                             this,
639                             SLOT(slotInvalidateMoving(KTextEditor::Document*)),
640                             Qt::UniqueConnection);
641         conn = connect(doc,
642                        SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document*)),
643                        this,
644                        SLOT(slotInvalidateMoving(KTextEditor::Document*)),
645                        Qt::UniqueConnection);
646     }
647 
648     connect(doc,
649             SIGNAL(markClicked(KTextEditor::Document*,KTextEditor::Mark,bool&)),
650             this,
651             SLOT(slotMarkClicked(KTextEditor::Document*,KTextEditor::Mark,bool&)),
652             Qt::UniqueConnection);
653     // clang-format on
654 }
655 
slotInvalidateMoving(KTextEditor::Document * doc)656 void KateBuildView::slotInvalidateMoving(KTextEditor::Document *doc)
657 {
658     QTreeWidgetItemIterator it(m_buildUi.errTreeWidget, QTreeWidgetItemIterator::All);
659     while (*it) {
660         QTreeWidgetItem *item = *it;
661         ++it;
662 
663         auto data = item->data(0, DataRole).value<ItemData>();
664         if (data.cursor && data.cursor->document() == doc) {
665             item->setData(0, DataRole, 0);
666         }
667     }
668 }
669 
slotMarkClicked(KTextEditor::Document * doc,KTextEditor::Mark mark,bool & handled)670 void KateBuildView::slotMarkClicked(KTextEditor::Document *doc, KTextEditor::Mark mark, bool &handled)
671 {
672     auto tree = m_buildUi.errTreeWidget;
673     QTreeWidgetItemIterator it(tree, QTreeWidgetItemIterator::All);
674     while (*it) {
675         QTreeWidgetItem *item = *it;
676         ++it;
677 
678         auto filename = item->data(0, Qt::UserRole).toString();
679         auto line = item->data(1, Qt::UserRole).toInt();
680         // prefer moving cursor's opinion if so available
681         auto data = item->data(0, DataRole).value<ItemData>();
682         if (data.cursor) {
683             line = data.cursor->line();
684         }
685         if (line - 1 == mark.line && QUrl::fromLocalFile(filename) == doc->url()) {
686             tree->blockSignals(true);
687             tree->setCurrentItem(item);
688             tree->scrollToItem(item, QAbstractItemView::PositionAtCenter);
689             tree->blockSignals(false);
690             handled = true;
691             break;
692         }
693     }
694 }
695 
slotViewChanged()696 void KateBuildView::slotViewChanged()
697 {
698     KTextEditor::View *activeView = m_win->activeView();
699     auto doc = activeView ? activeView->document() : nullptr;
700 
701     if (doc) {
702         addMarks(doc, m_showMarks->isChecked());
703     }
704 }
705 
slotDisplayOption()706 void KateBuildView::slotDisplayOption()
707 {
708     if (m_showMarks) {
709         if (!m_showMarks->isChecked()) {
710             clearMarks();
711         } else {
712             slotViewChanged();
713         }
714     }
715 }
716 
717 /******************************************************************/
docUrl()718 QUrl KateBuildView::docUrl()
719 {
720     KTextEditor::View *kv = m_win->activeView();
721     if (!kv) {
722         qDebug() << "no KTextEditor::View";
723         return QUrl();
724     }
725 
726     if (kv->document()->isModified()) {
727         kv->document()->save();
728     }
729     return kv->document()->url();
730 }
731 
732 /******************************************************************/
checkLocal(const QUrl & dir)733 bool KateBuildView::checkLocal(const QUrl &dir)
734 {
735     if (dir.path().isEmpty()) {
736         KMessageBox::sorry(nullptr, i18n("There is no file or directory specified for building."));
737         return false;
738     } else if (!dir.isLocalFile()) {
739         KMessageBox::sorry(nullptr,
740                            i18n("The file \"%1\" is not a local file. "
741                                 "Non-local files cannot be compiled.",
742                                 dir.path()));
743         return false;
744     }
745     return true;
746 }
747 
748 /******************************************************************/
clearBuildResults()749 void KateBuildView::clearBuildResults()
750 {
751     clearMarks();
752     m_buildUi.plainTextEdit->clear();
753     m_buildUi.errTreeWidget->clear();
754     m_stdOut.clear();
755     m_stdErr.clear();
756     m_numErrors = 0;
757     m_numWarnings = 0;
758     m_make_dir_stack.clear();
759 }
760 
761 /******************************************************************/
startProcess(const QString & dir,const QString & command)762 bool KateBuildView::startProcess(const QString &dir, const QString &command)
763 {
764     if (m_proc.state() != QProcess::NotRunning) {
765         return false;
766     }
767 
768     // clear previous runs
769     clearBuildResults();
770 
771     // activate the output tab
772     m_buildUi.u_tabWidget->setCurrentIndex(1);
773     m_displayModeBeforeBuild = m_buildUi.displayModeSlider->value();
774     m_buildUi.displayModeSlider->setValue(0);
775     m_win->showToolView(m_toolView);
776 
777     KTextEditor::View *kv = m_win->activeView();
778     if (kv) {
779         KTextEditor::ConfigInterface *ciface = qobject_cast<KTextEditor::ConfigInterface *>(kv);
780         if (ciface) {
781             QFont font = ciface->configValue(QStringLiteral("font")).value<QFont>();
782             m_buildUi.errTreeWidget->setFont(font);
783             m_buildUi.plainTextEdit->setFont(font);
784         }
785     }
786 
787     // set working directory
788     m_make_dir = dir;
789     m_make_dir_stack.push(m_make_dir);
790 
791     if (!QFile::exists(m_make_dir)) {
792         KMessageBox::error(nullptr, i18n("Cannot run command: %1\nWork path does not exist: %2", command, m_make_dir));
793         return false;
794     }
795 
796     // ninja build tool sends all output to stdout,
797     // so follow https://github.com/ninja-build/ninja/issues/1537 to separate ninja and compiler output
798     auto env = QProcessEnvironment::systemEnvironment();
799     const auto nstatus = QStringLiteral("NINJA_STATUS");
800     auto curr = env.value(nstatus, QStringLiteral("[%f/%t] "));
801     // add marker to search on later on
802     env.insert(nstatus, NinjaPrefix + curr);
803     m_ninjaBuildDetected = false;
804 
805     m_proc.setProcessEnvironment(env);
806     m_proc.setWorkingDirectory(m_make_dir);
807     m_proc.setShellCommand(command);
808     m_proc.start();
809 
810     if (!m_proc.waitForStarted(500)) {
811         KMessageBox::error(nullptr, i18n("Failed to run \"%1\". exitStatus = %2", command, m_proc.exitStatus()));
812         return false;
813     }
814 
815     m_buildUi.cancelBuildButton->setEnabled(true);
816     m_buildUi.cancelBuildButton2->setEnabled(true);
817     m_buildUi.buildAgainButton->setEnabled(false);
818     m_buildUi.buildAgainButton2->setEnabled(false);
819 
820     m_targetsUi->setCursor(Qt::BusyCursor);
821 
822     return true;
823 }
824 
825 /******************************************************************/
slotStop()826 bool KateBuildView::slotStop()
827 {
828     if (m_proc.state() != QProcess::NotRunning) {
829         m_buildCancelled = true;
830         QString msg = i18n("Building <b>%1</b> cancelled", m_currentlyBuildingTarget);
831         m_buildUi.buildStatusLabel->setText(msg);
832         m_buildUi.buildStatusLabel2->setText(msg);
833         m_proc.terminate();
834         return true;
835     }
836     return false;
837 }
838 
839 /******************************************************************/
slotBuildActiveTarget()840 void KateBuildView::slotBuildActiveTarget()
841 {
842     if (!m_targetsUi->targetsView->currentIndex().isValid()) {
843         slotSelectTarget();
844     } else {
845         buildCurrentTarget();
846     }
847 }
848 
849 /******************************************************************/
slotBuildPreviousTarget()850 void KateBuildView::slotBuildPreviousTarget()
851 {
852     if (!m_previousIndex.isValid()) {
853         slotSelectTarget();
854     } else {
855         m_targetsUi->targetsView->setCurrentIndex(m_previousIndex);
856         buildCurrentTarget();
857     }
858 }
859 
860 /******************************************************************/
slotBuildDefaultTarget()861 void KateBuildView::slotBuildDefaultTarget()
862 {
863     QModelIndex defaultTarget = m_targetsUi->targetsModel.defaultTarget(m_targetsUi->targetCombo->currentIndex());
864     m_targetsUi->targetsView->setCurrentIndex(defaultTarget);
865     buildCurrentTarget();
866 }
867 
868 /******************************************************************/
slotSelectTarget()869 void KateBuildView::slotSelectTarget()
870 {
871     SelectTargetView *dialog = new SelectTargetView(&(m_targetsUi->targetsModel));
872 
873     dialog->setCurrentIndex(m_targetsUi->targetsView->currentIndex());
874 
875     int result = dialog->exec();
876     if (result == QDialog::Accepted) {
877         m_targetsUi->targetsView->setCurrentIndex(dialog->currentIndex());
878         buildCurrentTarget();
879     }
880     delete dialog;
881     dialog = nullptr;
882 }
883 
884 /******************************************************************/
buildCurrentTarget()885 bool KateBuildView::buildCurrentTarget()
886 {
887     if (m_proc.state() != QProcess::NotRunning) {
888         displayBuildResult(i18n("Already building..."), KTextEditor::Message::Warning);
889         return false;
890     }
891 
892     QFileInfo docFInfo = docUrl().toLocalFile(); // docUrl() saves the current document
893 
894     QModelIndex ind = m_targetsUi->targetsView->currentIndex();
895     m_previousIndex = ind;
896     if (!ind.isValid()) {
897         KMessageBox::sorry(nullptr, i18n("No target available for building."));
898         return false;
899     }
900 
901     QString buildCmd = m_targetsUi->targetsModel.command(ind);
902     QString cmdName = m_targetsUi->targetsModel.cmdName(ind);
903     m_searchPaths = m_targetsUi->targetsModel.workDir(ind).split(QLatin1Char(';'));
904     QString workDir = m_searchPaths.isEmpty() ? QString() : m_searchPaths.first();
905     QString targetSet = m_targetsUi->targetsModel.targetName(ind);
906 
907     QString dir = workDir;
908     if (workDir.isEmpty()) {
909         dir = docFInfo.absolutePath();
910         if (dir.isEmpty()) {
911             KMessageBox::sorry(nullptr, i18n("There is no local file or directory specified for building."));
912             return false;
913         }
914     }
915 
916     // a single target can serve to build lots of projects with similar directory layout
917     if (m_projectPluginView) {
918         QFileInfo baseDir = m_projectPluginView->property("projectBaseDir").toString();
919         dir.replace(QStringLiteral("%B"), baseDir.absoluteFilePath());
920         dir.replace(QStringLiteral("%b"), baseDir.baseName());
921     }
922 
923     // Check if the command contains the file name or directory
924     if (buildCmd.contains(QLatin1String("%f")) || buildCmd.contains(QLatin1String("%d")) || buildCmd.contains(QLatin1String("%n"))) {
925         if (docFInfo.absoluteFilePath().isEmpty()) {
926             return false;
927         }
928 
929         buildCmd.replace(QStringLiteral("%n"), docFInfo.baseName());
930         buildCmd.replace(QStringLiteral("%f"), docFInfo.absoluteFilePath());
931         buildCmd.replace(QStringLiteral("%d"), docFInfo.absolutePath());
932     }
933     m_currentlyBuildingTarget = QStringLiteral("%1: %2").arg(targetSet, cmdName);
934     m_buildCancelled = false;
935     QString msg = i18n("Building target <b>%1</b> ...", m_currentlyBuildingTarget);
936     m_buildUi.buildStatusLabel->setText(msg);
937     m_buildUi.buildStatusLabel2->setText(msg);
938     return startProcess(dir, buildCmd);
939 }
940 
941 /******************************************************************/
displayBuildResult(const QString & msg,KTextEditor::Message::MessageType level)942 void KateBuildView::displayBuildResult(const QString &msg, KTextEditor::Message::MessageType level)
943 {
944     KTextEditor::View *kv = m_win->activeView();
945     if (!kv) {
946         return;
947     }
948 
949     delete m_infoMessage;
950     m_infoMessage = new KTextEditor::Message(xi18nc("@info", "<title>Make Results:</title><nl/>%1", msg), level);
951     m_infoMessage->setWordWrap(true);
952     m_infoMessage->setPosition(KTextEditor::Message::BottomInView);
953     m_infoMessage->setAutoHide(5000);
954     m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
955     m_infoMessage->setView(kv);
956     kv->document()->postMessage(m_infoMessage);
957 }
958 
959 /******************************************************************/
displayMessage(const QString & msg,KTextEditor::Message::MessageType level)960 void KateBuildView::displayMessage(const QString &msg, KTextEditor::Message::MessageType level)
961 {
962     KTextEditor::View *kv = m_win->activeView();
963     if (!kv) {
964         return;
965     }
966 
967     delete m_infoMessage;
968     m_infoMessage = new KTextEditor::Message(msg, level);
969     m_infoMessage->setWordWrap(true);
970     m_infoMessage->setPosition(KTextEditor::Message::BottomInView);
971     m_infoMessage->setAutoHide(8000);
972     m_infoMessage->setAutoHideMode(KTextEditor::Message::Immediate);
973     m_infoMessage->setView(kv);
974     kv->document()->postMessage(m_infoMessage);
975 }
976 
977 /******************************************************************/
slotProcExited(int exitCode,QProcess::ExitStatus)978 void KateBuildView::slotProcExited(int exitCode, QProcess::ExitStatus)
979 {
980     m_targetsUi->unsetCursor();
981     m_buildUi.cancelBuildButton->setEnabled(false);
982     m_buildUi.cancelBuildButton2->setEnabled(false);
983     m_buildUi.buildAgainButton->setEnabled(true);
984     m_buildUi.buildAgainButton2->setEnabled(true);
985 
986     QString buildStatus = i18n("Building <b>%1</b> completed.", m_currentlyBuildingTarget);
987 
988     // did we get any errors?
989     if (m_numErrors || m_numWarnings || (exitCode != 0)) {
990         m_buildUi.u_tabWidget->setCurrentIndex(1);
991         if (m_buildUi.displayModeSlider->value() == 0) {
992             m_buildUi.displayModeSlider->setValue(m_displayModeBeforeBuild > 0 ? m_displayModeBeforeBuild : 1);
993         }
994         m_buildUi.errTreeWidget->resizeColumnToContents(0);
995         m_buildUi.errTreeWidget->resizeColumnToContents(1);
996         m_buildUi.errTreeWidget->resizeColumnToContents(2);
997         m_buildUi.errTreeWidget->horizontalScrollBar()->setValue(0);
998         // m_buildUi.errTreeWidget->setSortingEnabled(true);
999         m_win->showToolView(m_toolView);
1000     }
1001 
1002     if (m_numErrors || m_numWarnings) {
1003         QStringList msgs;
1004         if (m_numErrors) {
1005             msgs << i18np("Found one error.", "Found %1 errors.", m_numErrors);
1006             buildStatus = i18n("Building <b>%1</b> had errors.", m_currentlyBuildingTarget);
1007         } else if (m_numWarnings) {
1008             msgs << i18np("Found one warning.", "Found %1 warnings.", m_numWarnings);
1009             buildStatus = i18n("Building <b>%1</b> had warnings.", m_currentlyBuildingTarget);
1010         }
1011         displayBuildResult(msgs.join(QLatin1Char('\n')), m_numErrors ? KTextEditor::Message::Error : KTextEditor::Message::Warning);
1012     } else if (exitCode != 0) {
1013         displayBuildResult(i18n("Build failed."), KTextEditor::Message::Warning);
1014     } else {
1015         displayBuildResult(i18n("Build completed without problems."), KTextEditor::Message::Positive);
1016     }
1017 
1018     if (!m_buildCancelled) {
1019         m_buildUi.buildStatusLabel->setText(buildStatus);
1020         m_buildUi.buildStatusLabel2->setText(buildStatus);
1021         m_buildCancelled = false;
1022         // add marks
1023         slotViewChanged();
1024     }
1025 }
1026 
1027 /******************************************************************/
slotReadReadyStdOut()1028 void KateBuildView::slotReadReadyStdOut()
1029 {
1030     // read data from procs stdout and add
1031     // the text to the end of the output
1032     // FIXME This works for utf8 but not for all charsets
1033     QString l = QString::fromUtf8(m_proc.readAllStandardOutput());
1034     l.remove(QLatin1Char('\r'));
1035     m_stdOut += l;
1036 
1037     // handle one line at a time
1038     do {
1039         const int end = m_stdOut.indexOf(QLatin1Char('\n'));
1040         if (end < 0) {
1041             break;
1042         }
1043 
1044         QString line = m_stdOut.mid(0, end);
1045         const bool ninjaOutput = line.startsWith(NinjaPrefix);
1046         m_ninjaBuildDetected |= ninjaOutput;
1047         if (ninjaOutput) {
1048             line = line.mid(NinjaPrefix.length());
1049         }
1050         m_buildUi.plainTextEdit->appendPlainText(line);
1051         // qDebug() << line;
1052 
1053         QRegularExpressionMatch match = m_newDirDetector.match(line);
1054 
1055         if (match.hasMatch()) {
1056             // qDebug() << "Enter/Exit dir found";
1057             QString newDir = match.captured(1);
1058             // qDebug () << "New dir = " << newDir;
1059 
1060             if ((m_make_dir_stack.size() > 1) && (m_make_dir_stack.top() == newDir)) {
1061                 m_make_dir_stack.pop();
1062                 newDir = m_make_dir_stack.top();
1063             } else {
1064                 m_make_dir_stack.push(newDir);
1065             }
1066 
1067             m_make_dir = newDir;
1068         } else if (m_ninjaBuildDetected && !ninjaOutput) {
1069             processLine(line);
1070         }
1071 
1072         m_stdOut.remove(0, end + 1);
1073     } while (1);
1074 }
1075 
1076 /******************************************************************/
slotReadReadyStdErr()1077 void KateBuildView::slotReadReadyStdErr()
1078 {
1079     // FIXME This works for utf8 but not for all charsets
1080     QString l = QString::fromUtf8(m_proc.readAllStandardError());
1081     l.remove(QLatin1Char('\r'));
1082     m_stdErr += l;
1083 
1084     do {
1085         const int end = m_stdErr.indexOf(QLatin1Char('\n'));
1086         if (end < 0) {
1087             break;
1088         }
1089 
1090         const QString line = m_stdErr.mid(0, end);
1091         m_buildUi.plainTextEdit->appendPlainText(line);
1092 
1093         processLine(line);
1094 
1095         m_stdErr.remove(0, end + 1);
1096     } while (1);
1097 }
1098 
1099 /******************************************************************/
processLine(const QString & line)1100 void KateBuildView::processLine(const QString &line)
1101 {
1102     // qDebug() << line ;
1103 
1104     // look for a filename
1105     QRegularExpressionMatch match = m_filenameDetector.match(line);
1106 
1107     if (!match.hasMatch()) {
1108         addError(QString(), QStringLiteral("0"), QString(), line);
1109         // kDebug() << "A filename was not found in the line ";
1110         return;
1111     }
1112 
1113     QString filename = match.captured(1);
1114     const QString line_n = match.captured(2);
1115     const QString col_n = match.captured(3);
1116     const QString msg = match.captured(4);
1117 
1118 #ifdef Q_OS_WIN
1119     // convert '\' to '/' so the concatenation works
1120     filename = QFileInfo(filename).filePath();
1121 #endif
1122 
1123     // qDebug() << "File Name:"<<m_make_dir << filename << " msg:"<< msg;
1124     // add path to file
1125     if (QFile::exists(m_make_dir + QLatin1Char('/') + filename)) {
1126         filename = m_make_dir + QLatin1Char('/') + filename;
1127     }
1128 
1129     // If we still do not have a file name try the extra search paths
1130     int i = 1;
1131     while (!QFile::exists(filename) && i < m_searchPaths.size()) {
1132         if (QFile::exists(m_searchPaths[i] + QLatin1Char('/') + filename)) {
1133             filename = m_searchPaths[i] + QLatin1Char('/') + filename;
1134         }
1135         i++;
1136     }
1137 
1138     // get canonical path, if possible, to avoid duplicated opened files
1139     auto canonicalFilePath(QFileInfo(filename).canonicalFilePath());
1140     if (!canonicalFilePath.isEmpty()) {
1141         filename = canonicalFilePath;
1142     }
1143 
1144     // Now we have the data we need show the error/warning
1145     addError(filename, line_n, col_n, msg);
1146 }
1147 
1148 /******************************************************************/
slotAddTargetClicked()1149 void KateBuildView::slotAddTargetClicked()
1150 {
1151     QModelIndex current = m_targetsUi->targetsView->currentIndex();
1152     if (current.parent().isValid()) {
1153         current = current.parent();
1154     }
1155     QModelIndex index = m_targetsUi->targetsModel.addCommand(current.row(), DefTargetName, DefBuildCmd);
1156     m_targetsUi->targetsView->setCurrentIndex(index);
1157 }
1158 
1159 /******************************************************************/
targetSetNew()1160 void KateBuildView::targetSetNew()
1161 {
1162     int row = m_targetsUi->targetsModel.addTargetSet(i18n("Target Set"), QString());
1163     QModelIndex buildIndex = m_targetsUi->targetsModel.addCommand(row, i18n("Build"), DefBuildCmd);
1164     m_targetsUi->targetsModel.addCommand(row, i18n("Clean"), DefCleanCmd);
1165     m_targetsUi->targetsModel.addCommand(row, i18n("Config"), DefConfigCmd);
1166     m_targetsUi->targetsModel.addCommand(row, i18n("ConfigClean"), DefConfClean);
1167     m_targetsUi->targetsView->setCurrentIndex(buildIndex);
1168 }
1169 
1170 /******************************************************************/
targetOrSetCopy()1171 void KateBuildView::targetOrSetCopy()
1172 {
1173     QModelIndex newIndex = m_targetsUi->targetsModel.copyTargetOrSet(m_targetsUi->targetsView->currentIndex());
1174     if (m_targetsUi->targetsModel.hasChildren(newIndex)) {
1175         m_targetsUi->targetsView->setCurrentIndex(newIndex.model()->index(0, 0, newIndex));
1176         return;
1177     }
1178     m_targetsUi->targetsView->setCurrentIndex(newIndex);
1179 }
1180 
1181 /******************************************************************/
targetDelete()1182 void KateBuildView::targetDelete()
1183 {
1184     QModelIndex current = m_targetsUi->targetsView->currentIndex();
1185     m_targetsUi->targetsModel.deleteItem(current);
1186 
1187     if (m_targetsUi->targetsModel.rowCount() == 0) {
1188         targetSetNew();
1189     }
1190 }
1191 
1192 /******************************************************************/
slotDisplayMode(int mode)1193 void KateBuildView::slotDisplayMode(int mode)
1194 {
1195     QTreeWidget *tree = m_buildUi.errTreeWidget;
1196     tree->setVisible(mode != 0);
1197     m_buildUi.plainTextEdit->setVisible(mode == 0);
1198 
1199     QString modeText;
1200     switch (mode) {
1201     case OnlyErrors:
1202         modeText = i18n("Only Errors");
1203         break;
1204     case ErrorsAndWarnings:
1205         modeText = i18n("Errors and Warnings");
1206         break;
1207     case ParsedOutput:
1208         modeText = i18n("Parsed Output");
1209         break;
1210     case FullOutput:
1211         modeText = i18n("Full Output");
1212         break;
1213     }
1214     m_buildUi.displayModeLabel->setText(modeText);
1215 
1216     if (mode < 1) {
1217         return;
1218     }
1219 
1220     const int itemCount = tree->topLevelItemCount();
1221 
1222     for (int i = 0; i < itemCount; i++) {
1223         QTreeWidgetItem *item = tree->topLevelItem(i);
1224 
1225         const ErrorCategory errorCategory = static_cast<ErrorCategory>(item->data(0, ErrorRole).toInt());
1226 
1227         switch (errorCategory) {
1228         case CategoryInfo:
1229             item->setHidden(mode > 1);
1230             break;
1231         case CategoryWarning:
1232             item->setHidden(mode > 2);
1233             break;
1234         case CategoryError:
1235             item->setHidden(false);
1236             break;
1237         }
1238     }
1239 }
1240 
1241 /******************************************************************/
slotPluginViewCreated(const QString & name,QObject * pluginView)1242 void KateBuildView::slotPluginViewCreated(const QString &name, QObject *pluginView)
1243 {
1244     // add view
1245     if (pluginView && name == QLatin1String("kateprojectplugin")) {
1246         m_projectPluginView = pluginView;
1247         slotAddProjectTarget();
1248         connect(pluginView, SIGNAL(projectMapChanged()), this, SLOT(slotProjectMapChanged()), Qt::UniqueConnection);
1249     }
1250 }
1251 
1252 /******************************************************************/
slotPluginViewDeleted(const QString & name,QObject *)1253 void KateBuildView::slotPluginViewDeleted(const QString &name, QObject *)
1254 {
1255     // remove view
1256     if (name == QLatin1String("kateprojectplugin")) {
1257         m_projectPluginView = nullptr;
1258         m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets"));
1259     }
1260 }
1261 
1262 /******************************************************************/
slotProjectMapChanged()1263 void KateBuildView::slotProjectMapChanged()
1264 {
1265     // only do stuff with valid project
1266     if (!m_projectPluginView) {
1267         return;
1268     }
1269     m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets"));
1270     slotAddProjectTarget();
1271 }
1272 
1273 /******************************************************************/
slotAddProjectTarget()1274 void KateBuildView::slotAddProjectTarget()
1275 {
1276     // only do stuff with valid project
1277     if (!m_projectPluginView) {
1278         return;
1279     }
1280     // query new project map
1281     QVariantMap projectMap = m_projectPluginView->property("projectMap").toMap();
1282 
1283     // do we have a valid map for build settings?
1284     QVariantMap buildMap = projectMap.value(QStringLiteral("build")).toMap();
1285     if (buildMap.isEmpty()) {
1286         return;
1287     }
1288 
1289     // Delete any old project plugin targets
1290     m_targetsUi->targetsModel.deleteTargetSet(i18n("Project Plugin Targets"));
1291 
1292     // handle build directory as relative to project file, if possible, see bug 413306
1293     QString projectsBuildDir = buildMap.value(QStringLiteral("directory")).toString();
1294     const QString projectsBaseDir = m_projectPluginView->property("projectBaseDir").toString();
1295     if (!projectsBaseDir.isEmpty()) {
1296         projectsBuildDir = QDir(projectsBaseDir).absoluteFilePath(projectsBuildDir);
1297     }
1298     const int set = m_targetsUi->targetsModel.addTargetSet(i18n("Project Plugin Targets"), projectsBuildDir);
1299 
1300     const QVariantList targetsets = buildMap.value(QStringLiteral("targets")).toList();
1301     for (const QVariant &targetVariant : targetsets) {
1302         QVariantMap targetMap = targetVariant.toMap();
1303         QString tgtName = targetMap[QStringLiteral("name")].toString();
1304         QString buildCmd = targetMap[QStringLiteral("build_cmd")].toString();
1305 
1306         if (tgtName.isEmpty() || buildCmd.isEmpty()) {
1307             continue;
1308         }
1309         m_targetsUi->targetsModel.addCommand(set, tgtName, buildCmd);
1310     }
1311 
1312     QModelIndex ind = m_targetsUi->targetsModel.index(set);
1313     if (!ind.model()->index(0, 0, ind).data().isValid()) {
1314         QString buildCmd = buildMap.value(QStringLiteral("build")).toString();
1315         QString cleanCmd = buildMap.value(QStringLiteral("clean")).toString();
1316         QString quickCmd = buildMap.value(QStringLiteral("quick")).toString();
1317         if (!buildCmd.isEmpty()) {
1318             // we have loaded an "old" project file (<= 4.12)
1319             m_targetsUi->targetsModel.addCommand(set, i18n("build"), buildCmd);
1320         }
1321         if (!cleanCmd.isEmpty()) {
1322             m_targetsUi->targetsModel.addCommand(set, i18n("clean"), cleanCmd);
1323         }
1324         if (!quickCmd.isEmpty()) {
1325             m_targetsUi->targetsModel.addCommand(set, i18n("quick"), quickCmd);
1326         }
1327     }
1328 }
1329 
1330 /******************************************************************/
eventFilter(QObject * obj,QEvent * event)1331 bool KateBuildView::eventFilter(QObject *obj, QEvent *event)
1332 {
1333     if (event->type() == QEvent::KeyPress) {
1334         QKeyEvent *ke = static_cast<QKeyEvent *>(event);
1335         if ((obj == m_toolView) && (ke->key() == Qt::Key_Escape)) {
1336             m_win->hideToolView(m_toolView);
1337             event->accept();
1338             return true;
1339         }
1340     }
1341     if ((event->type() == QEvent::Resize) && (obj == m_buildWidget)) {
1342         if (m_buildUi.u_tabWidget->currentIndex() == 1) {
1343             if ((m_outputWidgetWidth == 0) && m_buildUi.buildAgainButton->isVisible()) {
1344                 QSize msh = m_buildWidget->minimumSizeHint();
1345                 m_outputWidgetWidth = msh.width();
1346             }
1347         }
1348         bool useVertLayout = (m_buildWidget->width() < m_outputWidgetWidth);
1349         m_buildUi.buildAgainButton->setVisible(!useVertLayout);
1350         m_buildUi.cancelBuildButton->setVisible(!useVertLayout);
1351         m_buildUi.buildStatusLabel->setVisible(!useVertLayout);
1352         m_buildUi.buildAgainButton2->setVisible(useVertLayout);
1353         m_buildUi.cancelBuildButton2->setVisible(useVertLayout);
1354         m_buildUi.buildStatusLabel2->setVisible(useVertLayout);
1355     }
1356 
1357     return QObject::eventFilter(obj, event);
1358 }
1359 
1360 /******************************************************************/
handleEsc(QEvent * e)1361 void KateBuildView::handleEsc(QEvent *e)
1362 {
1363     if (!m_win) {
1364         return;
1365     }
1366 
1367     QKeyEvent *k = static_cast<QKeyEvent *>(e);
1368     if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {
1369         if (m_toolView->isVisible()) {
1370             m_win->hideToolView(m_toolView);
1371         }
1372     }
1373 }
1374 
1375 #include "plugin_katebuild.moc"
1376 
1377 // kate: space-indent on; indent-width 4; replace-tabs on;
1378