File indexing completed on 2024-05-05 05:51:22
0001 /* This file is part of the KDE project 0002 * 0003 * SPDX-FileCopyrightText: 2019 Dominik Haumann <dhaumann@kde.org> 0004 * 0005 * SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 #include "externaltoolsplugin.h" 0008 0009 #include "kateexternaltool.h" 0010 #include "kateexternaltoolscommand.h" 0011 #include "kateexternaltoolsconfigwidget.h" 0012 #include "kateexternaltoolsview.h" 0013 #include "katetoolrunner.h" 0014 0015 #include <KActionCollection> 0016 #include <KLocalizedString> 0017 #include <KTextEditor/Document> 0018 #include <KTextEditor/Editor> 0019 #include <KTextEditor/MainWindow> 0020 #include <KTextEditor/View> 0021 #include <QAction> 0022 #include <kparts/part.h> 0023 0024 #include <KAuthorized> 0025 #include <KConfig> 0026 #include <KConfigGroup> 0027 #include <KPluginFactory> 0028 #include <KXMLGUIFactory> 0029 0030 #include <QClipboard> 0031 #include <QGuiApplication> 0032 0033 #include <ktexteditor_utils.h> 0034 0035 static QString toolsConfigDir() 0036 { 0037 static const QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/kate/externaltools/"); 0038 return dir; 0039 } 0040 0041 static QList<KateExternalTool> readDefaultTools() 0042 { 0043 QDir dir(QStringLiteral(":/kconfig/externaltools-config/")); 0044 const QStringList entries = dir.entryList(QDir::NoDotAndDotDot | QDir::Files); 0045 0046 QList<KateExternalTool> tools; 0047 for (const auto &file : entries) { 0048 KConfig config(dir.absoluteFilePath(file)); 0049 KConfigGroup cg = config.group(QStringLiteral("General")); 0050 0051 KateExternalTool tool; 0052 tool.load(cg); 0053 tools.push_back(tool); 0054 } 0055 0056 return tools; 0057 } 0058 0059 K_PLUGIN_FACTORY_WITH_JSON(KateExternalToolsFactory, "externaltoolsplugin.json", registerPlugin<KateExternalToolsPlugin>();) 0060 0061 KateExternalToolsPlugin::KateExternalToolsPlugin(QObject *parent, const QVariantList &) 0062 : KTextEditor::Plugin(parent) 0063 { 0064 m_config = KSharedConfig::openConfig(QStringLiteral("kate-externaltoolspluginrc"), KConfig::NoGlobals, QStandardPaths::GenericConfigLocation); 0065 QDir().mkdir(toolsConfigDir()); 0066 0067 migrateConfig(); 0068 0069 // read built-in external tools from compiled-in resource file 0070 m_defaultTools = readDefaultTools(); 0071 0072 // load config from disk 0073 reload(); 0074 } 0075 0076 KateExternalToolsPlugin::~KateExternalToolsPlugin() 0077 { 0078 clearTools(); 0079 } 0080 0081 void KateExternalToolsPlugin::migrateConfig() 0082 { 0083 const QString oldFile = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, QStringLiteral("externaltools")); 0084 0085 if (!oldFile.isEmpty()) { 0086 KConfig oldConf(oldFile); 0087 KConfigGroup oldGroup(&oldConf, QStringLiteral("Global")); 0088 0089 const bool isFirstRun = oldGroup.readEntry("firststart", true); 0090 m_config->group(QStringLiteral("Global")).writeEntry("firststart", isFirstRun); 0091 m_config->sync(); 0092 0093 const int toolCount = oldGroup.readEntry("tools", 0); 0094 for (int i = 0; i < toolCount; ++i) { 0095 oldGroup = oldConf.group(QStringLiteral("Tool %1").arg(i)); 0096 const QString name = KateExternalTool::configFileName(oldGroup.readEntry("name")); 0097 const QString newConfPath = toolsConfigDir() + name; 0098 if (QFileInfo::exists(newConfPath)) { // Already migrated ? 0099 continue; 0100 } 0101 0102 KConfig newConfig(newConfPath); 0103 KConfigGroup newGroup = newConfig.group(QStringLiteral("General")); 0104 oldGroup.copyTo(&newGroup, KConfigBase::Persistent); 0105 newConfig.sync(); 0106 } 0107 0108 QFile::remove(oldFile); 0109 } 0110 } 0111 0112 QObject *KateExternalToolsPlugin::createView(KTextEditor::MainWindow *mainWindow) 0113 { 0114 KateExternalToolsPluginView *view = new KateExternalToolsPluginView(mainWindow, this); 0115 connect(this, &KateExternalToolsPlugin::externalToolsChanged, view, &KateExternalToolsPluginView::rebuildMenu); 0116 return view; 0117 } 0118 0119 void KateExternalToolsPlugin::clearTools() 0120 { 0121 delete m_command; 0122 m_command = nullptr; 0123 m_commands.clear(); 0124 qDeleteAll(m_tools); 0125 m_tools.clear(); 0126 } 0127 0128 void KateExternalToolsPlugin::addNewTool(KateExternalTool *tool) 0129 { 0130 m_tools.push_back(tool); 0131 if (tool->canExecute() && !tool->cmdname.isEmpty()) { 0132 m_commands.push_back(tool->cmdname); 0133 } 0134 if (KAuthorized::authorizeAction(QStringLiteral("shell_access"))) { 0135 m_command = new KateExternalToolsCommand(this); 0136 } 0137 } 0138 0139 void KateExternalToolsPlugin::removeTools(const std::vector<KateExternalTool *> &toRemove) 0140 { 0141 for (auto *tool : toRemove) { 0142 if (!tool) { 0143 continue; 0144 } 0145 0146 if (QString configFile = KateExternalTool::configFileName(tool->name); !configFile.isEmpty()) { 0147 QFile::remove(toolsConfigDir() + configFile); 0148 } 0149 0150 // remove old name variant, too 0151 if (QString configFile = KateExternalTool::configFileNameOldStyleOnlyForRemove(tool->name); !configFile.isEmpty()) { 0152 QFile::remove(toolsConfigDir() + configFile); 0153 } 0154 0155 delete tool; 0156 } 0157 0158 auto it = std::remove_if(m_tools.begin(), m_tools.end(), [&toRemove](KateExternalTool *tool) { 0159 return std::find(toRemove.cbegin(), toRemove.cend(), tool) != toRemove.cend(); 0160 }); 0161 m_tools.erase(it, m_tools.end()); 0162 } 0163 0164 void KateExternalToolsPlugin::save(KateExternalTool *tool, const QString &oldName) 0165 { 0166 const QString name = KateExternalTool::configFileName(tool->name); 0167 KConfig config(toolsConfigDir() + name); 0168 KConfigGroup cg = config.group(QStringLiteral("General")); 0169 tool->save(cg); 0170 config.sync(); 0171 0172 // The tool was renamed, remove the old config file 0173 if (!oldName.isEmpty()) { 0174 const QString oldFile = toolsConfigDir() + KateExternalTool::configFileName(oldName); 0175 QFile::remove(oldFile); 0176 0177 // remove old variant, too 0178 const QString oldFile2 = toolsConfigDir() + KateExternalTool::configFileNameOldStyleOnlyForRemove(oldName); 0179 QFile::remove(oldFile2); 0180 } 0181 } 0182 0183 void KateExternalToolsPlugin::reload() 0184 { 0185 KConfigGroup group(m_config, QStringLiteral("Global")); 0186 const bool firstStart = group.readEntry("firststart", true); 0187 0188 if (!firstStart) { 0189 // read user config 0190 QDir dir(toolsConfigDir()); 0191 const QStringList entries = dir.entryList(QDir::NoDotAndDotDot | QDir::Files); 0192 for (const auto &file : entries) { 0193 KConfig config(dir.absoluteFilePath(file)); 0194 KConfigGroup cg = config.group(QStringLiteral("General")); 0195 0196 auto t = new KateExternalTool(); 0197 t->load(cg); 0198 m_tools.push_back(t); 0199 } 0200 } else { 0201 // first start -> use system config 0202 for (const auto &tool : qAsConst(m_defaultTools)) { 0203 m_tools.push_back(new KateExternalTool(tool)); 0204 } 0205 } 0206 0207 // FIXME test for a command name first! 0208 for (auto *tool : qAsConst(m_tools)) { 0209 if (tool->canExecute() && !tool->cmdname.isEmpty()) { 0210 m_commands.push_back(tool->cmdname); 0211 } 0212 } 0213 0214 if (KAuthorized::authorizeAction(QStringLiteral("shell_access"))) { 0215 m_command = new KateExternalToolsCommand(this); 0216 } 0217 0218 Q_EMIT externalToolsChanged(); 0219 } 0220 0221 QStringList KateExternalToolsPlugin::commands() const 0222 { 0223 return m_commands; 0224 } 0225 0226 const KateExternalTool *KateExternalToolsPlugin::toolForCommand(const QString &cmd) const 0227 { 0228 for (auto tool : m_tools) { 0229 if (tool->cmdname == cmd) { 0230 return tool; 0231 } 0232 } 0233 return nullptr; 0234 } 0235 0236 const QList<KateExternalTool *> &KateExternalToolsPlugin::tools() const 0237 { 0238 return m_tools; 0239 } 0240 0241 QList<KateExternalTool> KateExternalToolsPlugin::defaultTools() const 0242 { 0243 return m_defaultTools; 0244 } 0245 0246 KateToolRunner *KateExternalToolsPlugin::runnerForTool(const KateExternalTool &tool, KTextEditor::View *view, bool executingSaveTrigger) 0247 { 0248 // expand the macros in command if any, 0249 // and construct a command with an absolute path 0250 auto mw = view->mainWindow(); 0251 0252 // save documents if requested 0253 if (!executingSaveTrigger) { 0254 if (tool.saveMode == KateExternalTool::SaveMode::CurrentDocument) { 0255 // only save if modified, to avoid unnecessary recompiles 0256 if (view->document()->isModified() && view->document()->url().isValid()) { 0257 view->document()->save(); 0258 } 0259 } else if (tool.saveMode == KateExternalTool::SaveMode::AllDocuments) { 0260 const auto guiClients = mw->guiFactory()->clients(); 0261 for (KXMLGUIClient *client : guiClients) { 0262 if (QAction *a = client->actionCollection()->action(QStringLiteral("file_save_all"))) { 0263 a->trigger(); 0264 break; 0265 } 0266 } 0267 } 0268 } 0269 0270 // copy tool 0271 std::unique_ptr<KateExternalTool> copy(new KateExternalTool(tool)); 0272 0273 // clear previous toolview data 0274 auto pluginView = viewForMainWindow(mw); 0275 pluginView->clearToolView(); 0276 0277 // expand macros 0278 auto editor = KTextEditor::Editor::instance(); 0279 copy->executable = editor->expandText(copy->executable, view); 0280 copy->arguments = editor->expandText(copy->arguments, view); 0281 copy->workingDir = editor->expandText(copy->workingDir, view); 0282 copy->input = editor->expandText(copy->input, view); 0283 0284 if (!copy->checkExec()) { 0285 Utils::showMessage( 0286 i18n("Failed to find executable '%1'. Please make sure the executable file exists and that variable names, if used, are correct", tool.executable), 0287 QIcon::fromTheme(QStringLiteral("system-run")), 0288 i18n("External Tools"), 0289 MessageType::Error, 0290 pluginView->mainWindow()); 0291 return nullptr; 0292 } 0293 0294 const QString messageText = copy->input.isEmpty() ? i18n("Running %1: %2 %3", copy->name, copy->executable, copy->arguments) 0295 : i18n("Running %1: %2 %3 with input %4", copy->name, copy->executable, copy->arguments, tool.input); 0296 0297 // use generic output view for status 0298 Utils::showMessage(messageText, QIcon::fromTheme(QStringLiteral("system-run")), i18n("External Tools"), MessageType::Info, pluginView->mainWindow()); 0299 0300 // Allocate runner on heap such that it lives as long as the child 0301 // process is running and does not block the main thread. 0302 return new KateToolRunner(std::move(copy), view, this); 0303 } 0304 0305 void KateExternalToolsPlugin::runTool(const KateExternalTool &tool, KTextEditor::View *view, bool executingSaveTrigger) 0306 { 0307 auto runner = runnerForTool(tool, view, executingSaveTrigger); 0308 if (!runner) { 0309 return; 0310 } 0311 // use QueuedConnection, since handleToolFinished deletes the runner 0312 connect(runner, &KateToolRunner::toolFinished, this, &KateExternalToolsPlugin::handleToolFinished, Qt::QueuedConnection); 0313 runner->run(); 0314 } 0315 0316 void KateExternalToolsPlugin::blockingRunTool(const KateExternalTool &tool, KTextEditor::View *view, bool executingSaveTrigger) 0317 { 0318 auto runner = runnerForTool(tool, view, executingSaveTrigger); 0319 if (!runner) { 0320 return; 0321 } 0322 connect(runner, &KateToolRunner::toolFinished, this, &KateExternalToolsPlugin::handleToolFinished); 0323 runner->run(); 0324 runner->waitForFinished(); 0325 } 0326 0327 void KateExternalToolsPlugin::handleToolFinished(KateToolRunner *runner, int exitCode, bool crashed) 0328 { 0329 auto view = runner->view(); 0330 if (view && !runner->outputData().isEmpty()) { 0331 switch (runner->tool()->outputMode) { 0332 case KateExternalTool::OutputMode::InsertAtCursor: { 0333 KTextEditor::Document::EditingTransaction transaction(view->document()); 0334 view->removeSelection(); 0335 view->insertText(runner->outputData()); 0336 break; 0337 } 0338 case KateExternalTool::OutputMode::ReplaceSelectedText: { 0339 KTextEditor::Document::EditingTransaction transaction(view->document()); 0340 view->removeSelectionText(); 0341 view->insertText(runner->outputData()); 0342 break; 0343 } 0344 case KateExternalTool::OutputMode::ReplaceCurrentDocument: { 0345 KTextEditor::Document::EditingTransaction transaction(view->document()); 0346 auto cursor = view->cursorPosition(); 0347 view->document()->clear(); 0348 view->insertText(runner->outputData()); 0349 view->setCursorPosition(cursor); 0350 break; 0351 } 0352 case KateExternalTool::OutputMode::AppendToCurrentDocument: { 0353 view->document()->insertText(view->document()->documentEnd(), runner->outputData()); 0354 break; 0355 } 0356 case KateExternalTool::OutputMode::InsertInNewDocument: { 0357 auto mainWindow = view->mainWindow(); 0358 auto newView = mainWindow->openUrl({}); 0359 newView->insertText(runner->outputData()); 0360 mainWindow->activateView(newView->document()); 0361 break; 0362 } 0363 case KateExternalTool::OutputMode::CopyToClipboard: { 0364 QGuiApplication::clipboard()->setText(runner->outputData()); 0365 break; 0366 } 0367 default: 0368 break; 0369 } 0370 } 0371 0372 if (view && runner->tool()->reload) { 0373 // updates-enabled trick: avoid some flicker 0374 const bool wereUpdatesEnabled = view->updatesEnabled(); 0375 view->setUpdatesEnabled(false); 0376 0377 Utils::KateScrollBarRestorer scrollRestorer(view); 0378 0379 // Reload doc 0380 view->document()->documentReload(); 0381 0382 scrollRestorer.restore(); 0383 0384 view->setUpdatesEnabled(wereUpdatesEnabled); 0385 } 0386 0387 KateExternalToolsPluginView *pluginView = runner->view() ? viewForMainWindow(runner->view()->mainWindow()) : nullptr; 0388 if (pluginView) { 0389 bool hasOutputInPane = false; 0390 if (runner->tool()->outputMode == KateExternalTool::OutputMode::DisplayInPane) { 0391 pluginView->setOutputData(runner->outputData()); 0392 hasOutputInPane = !runner->outputData().isEmpty(); 0393 } 0394 0395 QString messageBody; 0396 MessageType messageType = MessageType::Info; 0397 if (!runner->errorData().isEmpty()) { 0398 messageBody += i18n("Data written to stderr:\n"); 0399 messageBody += runner->errorData(); 0400 messageBody += QStringLiteral("\n"); 0401 messageType = MessageType::Warning; 0402 } 0403 if (crashed || exitCode != 0) { 0404 messageType = MessageType::Error; 0405 } 0406 0407 // print crash or exit code 0408 if (crashed) { 0409 messageBody += i18n("%1 crashed", runner->tool()->translatedName()); 0410 } else if (exitCode != 0) { 0411 messageBody += i18n("%1 finished with exit code %2", runner->tool()->translatedName(), exitCode); 0412 } 0413 0414 // use generic output view for status 0415 Utils::showMessage(messageBody, QIcon::fromTheme(QStringLiteral("system-run")), i18n("External Tools"), messageType, pluginView->mainWindow()); 0416 0417 // on successful execution => show output 0418 // otherwise the global output pane settings will ensure we see the error output 0419 if (!(crashed || exitCode != 0) && hasOutputInPane) { 0420 pluginView->showToolView(); 0421 } 0422 } 0423 0424 delete runner; 0425 } 0426 0427 int KateExternalToolsPlugin::configPages() const 0428 { 0429 return 1; 0430 } 0431 0432 KTextEditor::ConfigPage *KateExternalToolsPlugin::configPage(int number, QWidget *parent) 0433 { 0434 if (number == 0) { 0435 return new KateExternalToolsConfigWidget(parent, this); 0436 } 0437 return nullptr; 0438 } 0439 0440 void KateExternalToolsPlugin::registerPluginView(KateExternalToolsPluginView *view) 0441 { 0442 Q_ASSERT(!m_views.contains(view)); 0443 m_views.push_back(view); 0444 } 0445 0446 void KateExternalToolsPlugin::unregisterPluginView(KateExternalToolsPluginView *view) 0447 { 0448 Q_ASSERT(m_views.contains(view)); 0449 m_views.removeAll(view); 0450 } 0451 0452 KateExternalToolsPluginView *KateExternalToolsPlugin::viewForMainWindow(KTextEditor::MainWindow *mainWindow) const 0453 { 0454 for (auto view : m_views) { 0455 if (view->mainWindow() == mainWindow) { 0456 return view; 0457 } 0458 } 0459 return nullptr; 0460 } 0461 0462 #include "externaltoolsplugin.moc" 0463 #include "moc_externaltoolsplugin.cpp" 0464 0465 // kate: space-indent on; indent-width 4; replace-tabs on;