File indexing completed on 2024-05-05 04:40:55

0001 /*
0002     SPDX-FileCopyrightText: 2018 Amish K. Naidu <amhndu@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "scratchpad.h"
0008 #include "scratchpadview.h"
0009 #include "scratchpadjob.h"
0010 
0011 #include <debug.h>
0012 
0013 #include <interfaces/icore.h>
0014 #include <interfaces/iuicontroller.h>
0015 #include <interfaces/idocumentcontroller.h>
0016 #include <interfaces/iruncontroller.h>
0017 
0018 #include <KPluginFactory>
0019 #include <KLocalizedString>
0020 #include <KSharedConfig>
0021 #include <KConfigGroup>
0022 #include <KActionCollection>
0023 
0024 #include <QStandardItemModel>
0025 #include <QStandardPaths>
0026 #include <QDir>
0027 #include <QFileIconProvider>
0028 #include <QHash>
0029 
0030 #include <algorithm>
0031 
0032 K_PLUGIN_FACTORY_WITH_JSON(ScratchpadFactory, "scratchpad.json", registerPlugin<Scratchpad>(); )
0033 
0034 class ScratchpadToolViewFactory
0035     : public KDevelop::IToolViewFactory
0036 {
0037 public:
0038     explicit ScratchpadToolViewFactory(Scratchpad* plugin)
0039         : m_plugin(plugin)
0040     {}
0041 
0042     QWidget* create(QWidget* parent = nullptr) override
0043     {
0044         return new ScratchpadView(parent, m_plugin);
0045     }
0046 
0047     Qt::DockWidgetArea defaultPosition() const override
0048     {
0049         return Qt::LeftDockWidgetArea;
0050     }
0051 
0052     QString id() const override
0053     {
0054         return QStringLiteral("org.kdevelop.ScratchpadView");
0055     }
0056 
0057 private:
0058     Scratchpad* const m_plugin;
0059 };
0060 
0061 namespace {
0062 KConfigGroup scratchCommands()
0063 {
0064     return KSharedConfig::openConfig()->group("Scratchpad").group("Commands");
0065 }
0066 
0067 KConfigGroup mimeCommands()
0068 {
0069     return KSharedConfig::openConfig()->group("Scratchpad").group("Mime Commands");
0070 }
0071 
0072 QString commandForScratch(const QFileInfo& file)
0073 {
0074     if (scratchCommands().hasKey(file.fileName())) {
0075         return scratchCommands().readEntry(file.fileName());
0076     }
0077 
0078     const auto suffix = file.suffix();
0079     if (mimeCommands().hasKey(suffix)) {
0080         return mimeCommands().readEntry(suffix);
0081     }
0082 
0083     const static QHash<QString, QString> defaultCommands = {
0084         {QStringLiteral("cpp"), QStringLiteral("g++ -std=c++11 -o /tmp/a.out $f && /tmp/a.out")},
0085         {QStringLiteral("py"), QStringLiteral("python $f")},
0086         {QStringLiteral("js"), QStringLiteral("node $f")},
0087         {QStringLiteral("c"), QStringLiteral("gcc -o /tmp/a.out $f && /tmp/a.out")},
0088     };
0089 
0090     return defaultCommands.value(suffix);
0091 }
0092 }
0093 
0094 Scratchpad::Scratchpad(QObject* parent, const QVariantList& args)
0095     : KDevelop::IPlugin(QStringLiteral("scratchpad"), parent)
0096     , m_factory(new ScratchpadToolViewFactory(this))
0097     , m_model(new QStandardItemModel(this))
0098     , m_runAction(new QAction(this))
0099 {
0100     Q_UNUSED(args);
0101 
0102     qCDebug(PLUGIN_SCRATCHPAD) << "Scratchpad plugin is loaded!";
0103 
0104     core()->uiController()->addToolView(i18nc("@title:window", "Scratchpad"), m_factory);
0105 
0106     const QDir dataDir(dataDirectory());
0107     if (!dataDir.exists()) {
0108         qCDebug(PLUGIN_SCRATCHPAD) << "Creating directory" << dataDir;
0109         dataDir.mkpath(QStringLiteral("."));
0110     }
0111 
0112     const QFileInfoList scratches = dataDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
0113 
0114     for (const auto& fileInfo : scratches) {
0115         addFileToModel(fileInfo);
0116 
0117         // TODO if scratch is open (happens when restarting), set pretty name, below code doesn't work
0118 //         auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(fileInfo.absoluteFilePath()));
0119 //         if (document) {
0120 //             document->setPrettyName(i18n("scratch:%1", fileInfo.fileName()));
0121 //         }
0122     }
0123 }
0124 
0125 QStandardItemModel* Scratchpad::model() const
0126 {
0127     return m_model;
0128 }
0129 
0130 QString Scratchpad::dataDirectory()
0131 {
0132     const static QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
0133                                                               + QLatin1String("/kdevscratchpad/scratches/");
0134     return dir;
0135 }
0136 
0137 void Scratchpad::openScratch(const QModelIndex& index)
0138 {
0139     const QUrl scratchUrl = QUrl::fromLocalFile(index.data(FullPathRole).toString());
0140     auto* const document = core()->documentController()->openDocument(scratchUrl);
0141     document->setPrettyName(i18nc("prefix to distinguish scratch tabs", "scratch:%1", index.data().toString()));
0142 }
0143 
0144 void Scratchpad::runScratch(const QModelIndex& index)
0145 {
0146     qCDebug(PLUGIN_SCRATCHPAD) << "run" << index.data().toString();
0147 
0148     auto command = index.data(RunCommandRole).toString();
0149     command.replace(QLatin1String("$f"), index.data(FullPathRole).toString());
0150     if (!command.isEmpty()) {
0151         auto* job = new ScratchpadJob(command, index.data().toString(), this);
0152         core()->runController()->registerJob(job);
0153     }
0154 }
0155 
0156 void Scratchpad::removeScratch(const QModelIndex& index)
0157 {
0158     const QString path = index.data(FullPathRole).toString();
0159     if (auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(path))) {
0160         document->close();
0161     }
0162 
0163     if (QFile::remove(path)) {
0164         qCDebug(PLUGIN_SCRATCHPAD) << "removed" << index.data(FullPathRole);
0165         scratchCommands().deleteEntry(index.data().toString());
0166         m_model->removeRow(index.row());
0167     } else {
0168         emit actionFailed(i18n("Failed to remove scratch: %1", index.data().toString()));
0169     }
0170 }
0171 
0172 void Scratchpad::createScratch(const QString& name)
0173 {
0174     if (!m_model->findItems(name).isEmpty()) {
0175         emit actionFailed(i18n("Failed to create scratch: Name already in use"));
0176         return;
0177     }
0178 
0179     QFile file(dataDirectory() + name);
0180     if (!file.exists() && file.open(QIODevice::WriteOnly)) { // create a new file if it doesn't exist
0181         file.close();
0182     }
0183 
0184     if (file.exists()) {
0185         addFileToModel(file);
0186     } else {
0187         emit actionFailed(i18n("Failed to create new scratch"));
0188     }
0189 }
0190 
0191 void Scratchpad::renameScratch(const QModelIndex& index, const QString& previousName)
0192 {
0193     const QString newName = index.data().toString();
0194     if (newName.contains(QDir::separator())) {
0195         m_model->setData(index, previousName); // undo
0196         emit actionFailed(i18n("Failed to rename scratch: Names must not include path separator"));
0197         return;
0198     }
0199 
0200     const QString previousPath = dataDirectory() + previousName;
0201     const QString newPath = dataDirectory() + index.data().toString();
0202     if (previousPath == newPath) {
0203         return;
0204     }
0205 
0206     if (QFile::rename(previousPath, newPath)) {
0207         qCDebug(PLUGIN_SCRATCHPAD) << "renamed" << previousPath << "to" << newPath;
0208 
0209         m_model->setData(index, newPath, Scratchpad::FullPathRole);
0210         m_model->itemFromIndex(index)->setIcon(m_iconProvider.icon(QFileInfo(newPath)));
0211         auto config = scratchCommands();
0212         config.deleteEntry(previousName);
0213         config.writeEntry(newName, index.data(Scratchpad::RunCommandRole));
0214 
0215         // close old and re-open the closed document
0216         if (auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(previousPath))) {
0217             // FIXME is there a better way ? this feels hacky
0218             document->close();
0219             document = core()->documentController()->openDocument(QUrl::fromLocalFile(newPath));
0220             document->setPrettyName(i18nc("prefix to distinguish scratch tabs", "scratch:%1", index.data().toString()));
0221         }
0222     } else {
0223         qCWarning(PLUGIN_SCRATCHPAD) << "failed renaming" << previousPath << "to" << newPath;
0224         // rollback
0225         m_model->setData(index, previousName);
0226         emit actionFailed(i18n("Failed renaming scratch."));
0227     }
0228 }
0229 
0230 void Scratchpad::addFileToModel(const QFileInfo& fileInfo)
0231 {
0232     auto* const item = new QStandardItem(m_iconProvider.icon(fileInfo), fileInfo.fileName());
0233     item->setData(fileInfo.absoluteFilePath(), FullPathRole);
0234     const auto command = commandForScratch(fileInfo);
0235     item->setData(command, RunCommandRole);
0236     scratchCommands().writeEntry(item->text(), item->data(RunCommandRole));
0237     m_model->appendRow(item);
0238 }
0239 
0240 void Scratchpad::setCommand(const QModelIndex& index, const QString& command)
0241 {
0242     qCDebug(PLUGIN_SCRATCHPAD) << "set command" << index.data();
0243     m_model->setData(index, command, RunCommandRole);
0244     scratchCommands().writeEntry(index.data().toString(), command);
0245 
0246     mimeCommands().writeEntry(QFileInfo(index.data().toString()).suffix(), command);
0247 }
0248 
0249 QAction* Scratchpad::runAction() const
0250 {
0251     return m_runAction;
0252 }
0253 
0254 void Scratchpad::createActionsForMainWindow(Sublime::MainWindow* window, QString& xmlFile, KActionCollection& actions)
0255 {
0256     Q_UNUSED(window);
0257 
0258     xmlFile = QStringLiteral("kdevscratchpad.rc");
0259 
0260     // add to gui action collection, so that the shorcut is easily configurable
0261     // action setup done in ScratchpadView
0262     actions.addAction(QStringLiteral("run_scratch"), m_runAction);
0263 }
0264 
0265 
0266 #include "scratchpad.moc"
0267 #include "moc_scratchpad.cpp"