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"