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