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;