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