File indexing completed on 2024-04-28 04:38:41

0001 /*
0002     SPDX-FileCopyrightText: 2010 Milian Wolff <mail@milianw.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "externalscriptplugin.h"
0008 
0009 #include "externalscriptview.h"
0010 #include "externalscriptitem.h"
0011 #include "externalscriptjob.h"
0012 #include <debug.h>
0013 
0014 #include <interfaces/icore.h>
0015 #include <interfaces/iuicontroller.h>
0016 #include <interfaces/iruncontroller.h>
0017 #include <interfaces/idocumentcontroller.h>
0018 #include <interfaces/contextmenuextension.h>
0019 #include <interfaces/context.h>
0020 #include <interfaces/isession.h>
0021 
0022 #include <outputview/outputjob.h>
0023 
0024 #include <project/projectmodel.h>
0025 #include <util/path.h>
0026 
0027 #include <language/interfaces/editorcontext.h>
0028 
0029 #include <KPluginFactory>
0030 #include <KProcess>
0031 #include <KLocalizedString>
0032 
0033 #include <QAction>
0034 #include <QStandardItemModel>
0035 #include <QDBusConnection>
0036 #include <QMenu>
0037 
0038 K_PLUGIN_FACTORY_WITH_JSON(ExternalScriptFactory, "kdevexternalscript.json", registerPlugin<ExternalScriptPlugin>(); )
0039 
0040 class ExternalScriptViewFactory
0041     : public KDevelop::IToolViewFactory
0042 {
0043 public:
0044     explicit ExternalScriptViewFactory(ExternalScriptPlugin* plugin) : m_plugin(plugin) {}
0045 
0046     QWidget* create(QWidget* parent = nullptr) override
0047     {
0048         return new ExternalScriptView(m_plugin, parent);
0049     }
0050 
0051     Qt::DockWidgetArea defaultPosition() const override
0052     {
0053         return Qt::RightDockWidgetArea;
0054     }
0055 
0056     QString id() const override
0057     {
0058         return QStringLiteral("org.kdevelop.ExternalScriptView");
0059     }
0060 
0061 private:
0062     ExternalScriptPlugin* m_plugin;
0063 };
0064 
0065 // We extend ExternalScriptJob so that it deletes the temporarily created item on destruction
0066 class ExternalScriptJobOwningItem
0067     : public ExternalScriptJob
0068 {
0069     Q_OBJECT
0070 
0071 public:
0072     ExternalScriptJobOwningItem(ExternalScriptItem* item, const QUrl& url,
0073                                 ExternalScriptPlugin* parent) : ExternalScriptJob(item, url, parent)
0074         , m_item(item)
0075     {
0076     }
0077     ~ExternalScriptJobOwningItem() override
0078     {
0079         delete m_item;
0080     }
0081 
0082 private:
0083     ExternalScriptItem* m_item;
0084 };
0085 
0086 ExternalScriptPlugin* ExternalScriptPlugin::m_self = nullptr;
0087 
0088 ExternalScriptPlugin::ExternalScriptPlugin(QObject* parent, const QVariantList& /*args*/)
0089     : IPlugin(QStringLiteral("kdevexternalscript"), parent)
0090     , m_model(new QStandardItemModel(this))
0091     , m_factory(new ExternalScriptViewFactory(this))
0092 {
0093     Q_ASSERT(!m_self);
0094     m_self = this;
0095 
0096     QDBusConnection::sessionBus().registerObject(QStringLiteral(
0097                                                      "/org/kdevelop/ExternalScriptPlugin"), this,
0098                                                  QDBusConnection::ExportScriptableSlots);
0099 
0100     setXMLFile(QStringLiteral("kdevexternalscript.rc"));
0101 
0102     //BEGIN load config
0103     KConfigGroup config = getConfig();
0104     const auto groups = config.groupList();
0105     for (const QString& group : groups) {
0106         KConfigGroup script = config.group(group);
0107         if (script.hasKey("name") && script.hasKey("command")) {
0108             auto* item = new ExternalScriptItem;
0109             item->setKey(script.name());
0110             item->setText(script.readEntry("name"));
0111             item->setCommand(script.readEntry("command"));
0112             item->setInputMode(static_cast<ExternalScriptItem::InputMode>(script.readEntry("inputMode", 0u)));
0113             item->setOutputMode(static_cast<ExternalScriptItem::OutputMode>(script.readEntry("outputMode", 0u)));
0114             item->setErrorMode(static_cast<ExternalScriptItem::ErrorMode>(script.readEntry("errorMode", 0u)));
0115             item->setSaveMode(static_cast<ExternalScriptItem::SaveMode>(script.readEntry("saveMode", 0u)));
0116             item->setFilterMode(script.readEntry("filterMode", 0u));
0117             item->action()->setShortcut(QKeySequence(script.readEntry("shortcuts")));
0118             item->setShowOutput(script.readEntry("showOutput", true));
0119             m_model->appendRow(item);
0120         }
0121     }
0122 
0123     //END load config
0124 
0125     core()->uiController()->addToolView(i18n("External Scripts"), m_factory);
0126 
0127     connect(m_model, &QStandardItemModel::rowsAboutToBeRemoved,
0128             this, &ExternalScriptPlugin::rowsAboutToBeRemoved);
0129     connect(m_model, &QStandardItemModel::rowsInserted,
0130             this, &ExternalScriptPlugin::rowsInserted);
0131 
0132     const bool firstUse = config.readEntry("firstUse", true);
0133     if (firstUse) {
0134         // some example scripts
0135         auto* item = new ExternalScriptItem;
0136         item->setText(i18n("Quick Compile"));
0137         item->setCommand(QStringLiteral("g++ -o %b %f && ./%b"));
0138         m_model->appendRow(item);
0139 
0140     #ifndef Q_OS_WIN
0141         item = new ExternalScriptItem;
0142         item->setText(i18n("Sort Selection"));
0143         item->setCommand(QStringLiteral("sort"));
0144         item->setInputMode(ExternalScriptItem::InputSelectionOrDocument);
0145         item->setOutputMode(ExternalScriptItem::OutputReplaceSelectionOrDocument);
0146         item->setShowOutput(false);
0147         m_model->appendRow(item);
0148 
0149         item = new ExternalScriptItem;
0150         item->setText(i18n("Google Selection"));
0151         item->setCommand(QStringLiteral("xdg-open \"https://www.google.com/search?q=%s\""));
0152         item->setShowOutput(false);
0153         m_model->appendRow(item);
0154 
0155         item = new ExternalScriptItem;
0156         item->setText(i18n("Paste to Hastebin"));
0157         item->setCommand(QStringLiteral(
0158                              "a=$(cat); curl -X POST -s -d \"$a\" https://hastebin.com/documents | awk -F '\"' '{print \"https://hastebin.com/\"$4}' | xargs xdg-open ;"));
0159         item->setInputMode(ExternalScriptItem::InputSelectionOrDocument);
0160         item->setShowOutput(false);
0161         m_model->appendRow(item);
0162     #endif
0163 
0164         config.writeEntry("firstUse", false);
0165         config.sync();
0166     }
0167 }
0168 
0169 ExternalScriptPlugin* ExternalScriptPlugin::self()
0170 {
0171     return m_self;
0172 }
0173 
0174 ExternalScriptPlugin::~ExternalScriptPlugin()
0175 {
0176     m_self = nullptr;
0177 }
0178 
0179 KDevelop::ContextMenuExtension ExternalScriptPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
0180 {
0181     m_urls.clear();
0182 
0183     int folderCount = 0;
0184 
0185     if (context->type() == KDevelop::Context::FileContext) {
0186         auto* filectx = static_cast<KDevelop::FileContext*>(context);
0187         m_urls = filectx->urls();
0188     } else if (context->type() == KDevelop::Context::ProjectItemContext) {
0189         auto* projctx = static_cast<KDevelop::ProjectItemContext*>(context);
0190         const auto items = projctx->items();
0191         for (KDevelop::ProjectBaseItem* item : items) {
0192             if (item->file()) {
0193                 m_urls << item->file()->path().toUrl();
0194             } else if (item->folder()) {
0195                 m_urls << item->folder()->path().toUrl();
0196                 folderCount++;
0197             }
0198         }
0199     } else if (context->type() == KDevelop::Context::EditorContext) {
0200         auto* econtext = static_cast<KDevelop::EditorContext*>(context);
0201         m_urls << econtext->url();
0202     }
0203 
0204     if (!m_urls.isEmpty()) {
0205         KDevelop::ContextMenuExtension ext;
0206         QMenu* menu = nullptr;
0207 
0208         for (int row = 0; row < m_model->rowCount(); ++row) {
0209             auto* item = dynamic_cast<ExternalScriptItem*>(m_model->item(row));
0210             Q_ASSERT(item);
0211 
0212             if (context->type() != KDevelop::Context::EditorContext) {
0213                 // filter scripts that depend on an opened document
0214                 // if the context menu was not requested inside the editor
0215                 if (item->performParameterReplacement() && item->command().contains(QLatin1String("%s"))) {
0216                     continue;
0217                 } else if (item->inputMode() == ExternalScriptItem::InputSelectionOrNone) {
0218                     continue;
0219                 }
0220             }
0221 
0222             if (folderCount == m_urls.count()) {
0223                 // when only folders filter items that don't have %d parameter (or another parameter)
0224                 if (item->performParameterReplacement() &&
0225                     (!item->command().contains(QLatin1String("%d")) ||
0226                      item->command().contains(QLatin1String("%s")) ||
0227                      item->command().contains(QLatin1String("%u")) ||
0228                      item->command().contains(QLatin1String("%f")) ||
0229                      item->command().contains(QLatin1String("%b")) ||
0230                      item->command().contains(QLatin1String("%n"))
0231                     )
0232                 ) {
0233                     continue;
0234                 }
0235             }
0236 
0237             if (!menu) {
0238                 menu = new QMenu(i18nc("@title:menu", "External Scripts"), parent);
0239             }
0240 
0241             auto* scriptAction = new QAction(item->text(), menu);
0242             scriptAction->setData(QVariant::fromValue<ExternalScriptItem*>(item));
0243             connect(scriptAction, &QAction::triggered, this, &ExternalScriptPlugin::executeScriptFromContextMenu);
0244             menu->addAction(scriptAction);
0245         }
0246 
0247         if (menu) {
0248             ext.addAction(KDevelop::ContextMenuExtension::ExtensionGroup, menu->menuAction());
0249         }
0250 
0251         return ext;
0252     }
0253 
0254     return KDevelop::IPlugin::contextMenuExtension(context, parent);
0255 }
0256 
0257 void ExternalScriptPlugin::unload()
0258 {
0259     core()->uiController()->removeToolView(m_factory);
0260     KDevelop::IPlugin::unload();
0261 }
0262 
0263 KConfigGroup ExternalScriptPlugin::getConfig() const
0264 {
0265     return KSharedConfig::openConfig()->group("External Scripts");
0266 }
0267 
0268 QStandardItemModel* ExternalScriptPlugin::model() const
0269 {
0270     return m_model;
0271 }
0272 
0273 void ExternalScriptPlugin::execute(ExternalScriptItem* item, const QUrl& url) const
0274 {
0275     auto* job = new ExternalScriptJob(item, url, const_cast<ExternalScriptPlugin*>(this));
0276 
0277     KDevelop::ICore::self()->runController()->registerJob(job);
0278 }
0279 
0280 void ExternalScriptPlugin::execute(ExternalScriptItem* item) const
0281 {
0282     auto document = KDevelop::ICore::self()->documentController()->activeDocument();
0283     execute(item, document ? document->url() : QUrl());
0284 }
0285 
0286 bool ExternalScriptPlugin::executeCommand(const QString& command, const QString& workingDirectory) const
0287 {
0288     auto* item = new ExternalScriptItem;
0289     item->setCommand(command);
0290     item->setWorkingDirectory(workingDirectory);
0291     item->setPerformParameterReplacement(false);
0292     qCDebug(PLUGIN_EXTERNALSCRIPT) << "executing command " << command << " in dir " << workingDirectory <<
0293         " as external script";
0294     auto* job =
0295         new ExternalScriptJobOwningItem(item, QUrl(), const_cast<ExternalScriptPlugin*>(this));
0296     // When a command is executed, for example through the terminal, we don't want the command output to be risen
0297     job->setVerbosity(KDevelop::OutputJob::Silent);
0298 
0299     KDevelop::ICore::self()->runController()->registerJob(job);
0300     return true;
0301 }
0302 
0303 QString ExternalScriptPlugin::executeCommandSync(const QString& command, const QString& workingDirectory) const
0304 {
0305     qCDebug(PLUGIN_EXTERNALSCRIPT) << "executing command " << command << " in working-dir " << workingDirectory;
0306     KProcess process;
0307     process.setWorkingDirectory(workingDirectory);
0308     process.setShellCommand(command);
0309     process.setOutputChannelMode(KProcess::OnlyStdoutChannel);
0310     process.execute();
0311     return QString::fromLocal8Bit(process.readAll());
0312 }
0313 
0314 void ExternalScriptPlugin::executeScriptFromActionData() const
0315 {
0316     auto* action = qobject_cast<QAction*>(sender());
0317     Q_ASSERT(action);
0318 
0319     auto* item = action->data().value<ExternalScriptItem*>();
0320     Q_ASSERT(item);
0321 
0322     execute(item);
0323 }
0324 
0325 void ExternalScriptPlugin::executeScriptFromContextMenu() const
0326 {
0327     auto* action = qobject_cast<QAction*>(sender());
0328     Q_ASSERT(action);
0329 
0330     auto* item = action->data().value<ExternalScriptItem*>();
0331     Q_ASSERT(item);
0332 
0333     for (const QUrl& url : m_urls) {
0334         KDevelop::ICore::self()->documentController()->openDocument(url);
0335         execute(item, url);
0336     }
0337 }
0338 
0339 void ExternalScriptPlugin::rowsInserted(const QModelIndex& /*parent*/, int start, int end)
0340 {
0341     setupKeys(start, end);
0342     for (int row = start; row <= end; ++row) {
0343         saveItemForRow(row);
0344     }
0345 }
0346 
0347 void ExternalScriptPlugin::rowsAboutToBeRemoved(const QModelIndex& /*parent*/, int start, int end)
0348 {
0349     KConfigGroup config = getConfig();
0350     for (int row = start; row <= end; ++row) {
0351         const ExternalScriptItem* const item = static_cast<ExternalScriptItem*>(m_model->item(row));
0352         KConfigGroup child = config.group(item->key());
0353         qCDebug(PLUGIN_EXTERNALSCRIPT) << "removing config group:" << child.name();
0354         child.deleteGroup();
0355     }
0356 
0357     config.sync();
0358 }
0359 
0360 void ExternalScriptPlugin::saveItem(const ExternalScriptItem* item)
0361 {
0362     const QModelIndex index = m_model->indexFromItem(item);
0363     Q_ASSERT(index.isValid());
0364 
0365     getConfig().group(item->key()).deleteGroup(); // delete the previous group
0366     setupKeys(index.row(), index.row());
0367     saveItemForRow(index.row()); // save the new group
0368 }
0369 
0370 void ExternalScriptPlugin::saveItemForRow(int row)
0371 {
0372     const QModelIndex idx = m_model->index(row, 0);
0373     Q_ASSERT(idx.isValid());
0374 
0375     auto* item = dynamic_cast<ExternalScriptItem*>(m_model->item(row));
0376     Q_ASSERT(item);
0377 
0378     qCDebug(PLUGIN_EXTERNALSCRIPT) << "save extern script:" << item << idx;
0379     KConfigGroup config = getConfig().group(item->key());
0380     config.writeEntry("name", item->text());
0381     config.writeEntry("command", item->command());
0382     config.writeEntry("inputMode", ( uint ) item->inputMode());
0383     config.writeEntry("outputMode", ( uint ) item->outputMode());
0384     config.writeEntry("errorMode", ( uint ) item->errorMode());
0385     config.writeEntry("saveMode", ( uint ) item->saveMode());
0386     config.writeEntry("shortcuts", item->action()->shortcut().toString());
0387     config.writeEntry("showOutput", item->showOutput());
0388     config.writeEntry("filterMode", item->filterMode());
0389     config.sync();
0390 }
0391 
0392 void ExternalScriptPlugin::setupKeys(int start, int end)
0393 {
0394     QStringList keys = getConfig().groupList();
0395 
0396     for (int row = start; row <= end; ++row) {
0397         auto* const item = static_cast<ExternalScriptItem*>(m_model->item(row));
0398 
0399         int nextSuffix = 2;
0400         QString keyCandidate = item->text();
0401         for (; keys.contains(keyCandidate); ++nextSuffix) {
0402             keyCandidate = item->text() + QString::number(nextSuffix);
0403         }
0404 
0405         qCDebug(PLUGIN_EXTERNALSCRIPT) << "set key" << keyCandidate << "for" << item << item->command();
0406         item->setKey(keyCandidate);
0407         keys.push_back(keyCandidate);
0408     }
0409 }
0410 
0411 #include "externalscriptplugin.moc"
0412 #include "moc_externalscriptplugin.cpp"