Commit 37f68b8d authored by Amish Naidu's avatar Amish Naidu

Add scratchpad plugin

Summary:
Adds a scratchpad plugin, which allows you to keep "scratches" of code/text
to experiment or quickly run something without the need for a project.
The plugin adds a new tool-view, which will maintain a list of your scratches
as well as allowing you to compile and run them.
The scratches live in the directory `scratches` in the data directory and
are regular documents so we get all the editing convenience of code-completion
and diagnostics.
Commands used to run them are saved per-scratches and new scratches use the last
used command for that file type/suffix.

FEATURE: 176389

Test Plan:
Add the tool-view on the left tool bar and try creating and using the scratches.
Currently no automated tests.

Reviewers: #kdevelop, kfunk

Reviewed By: #kdevelop, kfunk

Subscribers: aaronpuchert, kfunk, gregormi, brauch, kdevelop-devel

Tags: #kdevelop

Differential Revision: https://phabricator.kde.org/D16484
parent 2bd71e5f
......@@ -130,6 +130,7 @@ add_subdirectory(sourceformatter)
add_subdirectory(standardoutputview)
add_subdirectory(switchtobuddy)
add_subdirectory(testview)
add_subdirectory(scratchpad)
ecm_optional_add_subdirectory(classbrowser)
ecm_optional_add_subdirectory(executeplasmoid)
ecm_optional_add_subdirectory(ghprovider)
......
add_definitions(-DTRANSLATION_DOMAIN=\"kdevscratchpad\")
set(scratchpad_SRCS
scratchpad.cpp
scratchpadview.cpp
scratchpadjob.cpp
)
ki18n_wrap_ui(scratchpad_SRCS scratchpadview.ui)
declare_qt_logging_category(scratchpad_SRCS
TYPE PLUGIN
IDENTIFIER PLUGIN_SCRATCHPAD
CATEGORY_BASENAME "scratchpad"
)
kdevplatform_add_plugin(kdevscratchpad
JSON scratchpad.json
SOURCES ${scratchpad_SRCS}
)
target_link_libraries(kdevscratchpad
KDev::Interfaces
KDev::Util
KDev::OutputView
)
/* This file is part of KDevelop
*
* Copyright 2018 Amish K. Naidu <amhndu@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#ifndef EMPTYMESSAGELISTVIEW_H
#define EMPTYMESSAGELISTVIEW_H
#include <QListView>
// subclass to show a message when the list is empty
class EmptyMessageListView
: public QListView
{
public:
EmptyMessageListView(QWidget* parent);
void setEmptyMessage(const QString& message);
protected:
void paintEvent(QPaintEvent* event) override;
private:
QString m_message;
};
#endif // EMPTYMESSAGELISTVIEW_H
/* This file is part of KDevelop
*
* Copyright 2018 Amish K. Naidu <amhndu@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#include "scratchpad.h"
#include "scratchpadview.h"
#include "scratchpadjob.h"
#include <debug.h>
#include <interfaces/icore.h>
#include <interfaces/iuicontroller.h>
#include <interfaces/idocumentcontroller.h>
#include <interfaces/iruncontroller.h>
#include <KPluginFactory>
#include <KLocalizedString>
#include <KSharedConfig>
#include <KConfigGroup>
#include <QStandardItemModel>
#include <QStandardPaths>
#include <QDir>
#include <QFileIconProvider>
#include <QHash>
#include <algorithm>
K_PLUGIN_FACTORY_WITH_JSON(ScratchpadFactory, "scratchpad.json", registerPlugin<Scratchpad>(); )
class ScratchpadToolViewFactory
: public KDevelop::IToolViewFactory
{
public:
explicit ScratchpadToolViewFactory(Scratchpad* plugin)
: m_plugin(plugin)
{}
QWidget* create(QWidget* parent = nullptr) override
{
return new ScratchpadView(parent, m_plugin);
}
Qt::DockWidgetArea defaultPosition() override
{
return Qt::LeftDockWidgetArea;
}
QString id() const override
{
return QStringLiteral("org.kdevelop.scratchpad");
}
private:
Scratchpad* const m_plugin;
};
namespace {
KConfigGroup scratchCommands()
{
return KSharedConfig::openConfig()->group("Scratchpad").group("Commands");
}
KConfigGroup mimeCommands()
{
return KSharedConfig::openConfig()->group("Scratchpad").group("Mime Commands");
}
QString commandForScratch(const QFileInfo& file)
{
if (scratchCommands().hasKey(file.fileName())) {
return scratchCommands().readEntry(file.fileName());
}
const auto suffix = file.suffix();
if (mimeCommands().hasKey(suffix)) {
return mimeCommands().readEntry(suffix);
}
const static QHash<QString, QString> defaultCommands = {
{QStringLiteral("cpp"), QStringLiteral("g++ -std=c++11 -o /tmp/a.out $f && /tmp/a.out")},
{QStringLiteral("py"), QStringLiteral("python $f")},
{QStringLiteral("js"), QStringLiteral("node $f")},
{QStringLiteral("c"), QStringLiteral("gcc -o /tmp/a.out $f && /tmp/a.out")},
};
return defaultCommands.value(suffix);
}
}
Scratchpad::Scratchpad(QObject* parent, const QVariantList& args)
: KDevelop::IPlugin(QStringLiteral("scratchpad"), parent)
, m_factory(new ScratchpadToolViewFactory(this))
, m_model(new QStandardItemModel(this))
{
Q_UNUSED(args);
qCDebug(PLUGIN_SCRATCHPAD) << "Scratchpad plugin is loaded!";
core()->uiController()->addToolView(i18n("Scratchpad"), m_factory);
const QDir dataDir(dataDirectory());
if (!dataDir.exists()) {
qCDebug(PLUGIN_SCRATCHPAD) << "Creating directory" << dataDir;
dataDir.mkpath(QStringLiteral("."));
}
const QFileInfoList scratches = dataDir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
for (const auto& fileInfo : scratches) {
addFileToModel(fileInfo);
// TODO if scratch is open (happens when restarting), set pretty name, below code doesn't work
// auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(fileInfo.absoluteFilePath()));
// if (document) {
// document->setPrettyName(i18n("scratch:%1", fileInfo.fileName()));
// }
}
}
QStandardItemModel* Scratchpad::model() const
{
return m_model;
}
QString Scratchpad::dataDirectory()
{
const static QString dir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)
+ QLatin1String("/kdevscratchpad/scratches/");
return dir;
}
void Scratchpad::openScratch(const QModelIndex& index)
{
const QUrl scratchUrl = QUrl::fromLocalFile(index.data(FullPathRole).toString());
auto* const document = core()->documentController()->openDocument(scratchUrl);
document->setPrettyName(i18nc("prefix to distinguish scratch tabs", "scratch:%1", index.data().toString()));
}
void Scratchpad::runScratch(const QModelIndex& index)
{
qCDebug(PLUGIN_SCRATCHPAD) << "run" << index.data().toString();
auto command = index.data(RunCommandRole).toString();
command.replace(QLatin1String("$f"), index.data(FullPathRole).toString());
if (!command.isEmpty()) {
auto* job = new ScratchpadJob(command, index.data().toString(), this);
core()->runController()->registerJob(job);
}
}
void Scratchpad::removeScratch(const QModelIndex& index)
{
const QString path = index.data(FullPathRole).toString();
if (auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(path))) {
document->close();
}
if (QFile::remove(path)) {
qCDebug(PLUGIN_SCRATCHPAD) << "removed" << index.data(FullPathRole);
scratchCommands().deleteEntry(index.data().toString());
m_model->removeRow(index.row());
} else {
emit actionFailed(i18n("Failed to remove scratch: %1", index.data().toString()));
}
}
void Scratchpad::createScratch(const QString& name)
{
if (!m_model->findItems(name).isEmpty()) {
emit actionFailed(i18n("Failed to create scratch: Name already in use"));
return;
}
QFile file(dataDirectory() + name);
if (file.open(QIODevice::NewOnly)) { // create a new file
file.close();
}
if (file.exists()) {
addFileToModel(file);
} else {
emit actionFailed(i18n("Failed to create new scratch"));
}
}
void Scratchpad::renameScratch(const QModelIndex& index, const QString& previousName)
{
const QString newName = index.data().toString();
if (newName.contains(QDir::separator())) {
m_model->setData(index, previousName); // undo
emit actionFailed(i18n("Failed to rename scratch: Names must not include path seperator"));
return;
}
const QString previousPath = dataDirectory() + previousName;
const QString newPath = dataDirectory() + index.data().toString();
if (previousPath == newPath) {
return;
}
if (QFile::rename(previousPath, newPath)) {
qCDebug(PLUGIN_SCRATCHPAD) << "renamed" << previousPath << "to" << newPath;
m_model->setData(index, newPath, Scratchpad::FullPathRole);
m_model->itemFromIndex(index)->setIcon(m_iconProvider.icon(QFileInfo(newPath)));
auto config = scratchCommands();
config.deleteEntry(previousName);
config.writeEntry(newName, index.data(Scratchpad::RunCommandRole));
// close old and re-open the closed document
if (auto* document = core()->documentController()->documentForUrl(QUrl::fromLocalFile(previousPath))) {
// FIXME is there a better way ? this feels hacky
document->close();
document = core()->documentController()->openDocument(QUrl::fromLocalFile(newPath));
document->setPrettyName(i18nc("prefix to distinguish scratch tabs", "scratch:%1", index.data().toString()));
}
} else {
qCWarning(PLUGIN_SCRATCHPAD) << "failed renaming" << previousPath << "to" << newPath;
// rollback
m_model->setData(index, previousName);
emit actionFailed(i18n("Failed renaming scratch."));
}
}
void Scratchpad::addFileToModel(const QFileInfo& fileInfo)
{
auto* const item = new QStandardItem(m_iconProvider.icon(fileInfo), fileInfo.fileName());
item->setData(fileInfo.absoluteFilePath(), FullPathRole);
const auto command = commandForScratch(fileInfo);
item->setData(command, RunCommandRole);
scratchCommands().writeEntry(item->text(), item->data(RunCommandRole));
m_model->appendRow(item);
}
void Scratchpad::setCommand(const QModelIndex& index, const QString& command)
{
qCDebug(PLUGIN_SCRATCHPAD) << "set command" << index.data();
m_model->setData(index, command, RunCommandRole);
scratchCommands().writeEntry(index.data().toString(), command);
mimeCommands().writeEntry(QFileInfo(index.data().toString()).suffix(), command);
}
#include "scratchpad.moc"
/* This file is part of KDevelop
*
* Copyright 2018 Amish K. Naidu <amhndu@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#ifndef SCRATCHPAD_H
#define SCRATCHPAD_H
#include <interfaces/iplugin.h>
#include <QFileIconProvider>
class ScratchpadToolViewFactory;
class QStandardItemModel;
class QModelIndex;
class QFileInfo;
class QString;
class Scratchpad
: public KDevelop::IPlugin
{
Q_OBJECT
public:
Scratchpad(QObject* parent, const QVariantList& args);
QStandardItemModel* model() const;
static QString dataDirectory();
enum ExtraRoles {
FullPathRole = Qt::UserRole + 1,
RunCommandRole,
};
public Q_SLOTS:
void openScratch(const QModelIndex& index);
void runScratch(const QModelIndex& index);
void removeScratch(const QModelIndex& index);
void createScratch(const QString& name);
void renameScratch(const QModelIndex& index, const QString& previousName);
void setCommand(const QModelIndex& index, const QString& command);
Q_SIGNALS:
void actionFailed(const QString& message);
private:
void addFileToModel(const QFileInfo& fileInfo);
ScratchpadToolViewFactory* m_factory;
QStandardItemModel* m_model;
QFileIconProvider m_iconProvider;
};
#endif // SCRATCHPAD_H
{
"KPlugin": {
"Description": "Scratchpad lets you quickly run and experiment with code without a full project, and even store todos.",
"Id": "scratchpad",
"Name": "Scratchpad",
"ServiceTypes": [
"KDevelop/Plugin"
]
},
"X-KDevelop-Category": "Global",
"X-KDevelop-Mode": "GUI"
}
/* This file is part of KDevelop
*
* Copyright 2018 Amish K. Naidu <amhndu@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#include "scratchpadjob.h"
#include "scratchpad.h"
#include <debug.h>
#include <outputview/outputmodel.h>
#include <util/processlinemaker.h>
#include <KProcess>
#include <KLocalizedString>
#include <QMetaEnum>
ScratchpadJob::ScratchpadJob(const QString& command, const QString& title, QObject* parent)
: KDevelop::OutputJob(parent)
, m_process(new KProcess(this))
, m_lineMaker(new KDevelop::ProcessLineMaker(m_process, this))
{
qCDebug(PLUGIN_SCRATCHPAD) << "Creating job for" << title;
setCapabilities(Killable);
if (!command.isEmpty()) {
m_process->setShellCommand(command);
setStandardToolView(KDevelop::IOutputView::RunView);
setTitle(i18nc("prefix to distinguish scratch tabs", "scratch:%1", title));
auto* model = new KDevelop::OutputModel(this);
setModel(model);
connect(m_lineMaker, &KDevelop::ProcessLineMaker::receivedStdoutLines,
model, &KDevelop::OutputModel::appendLines);
connect(m_lineMaker, &KDevelop::ProcessLineMaker::receivedStderrLines,
model, &KDevelop::OutputModel::appendLines);
m_process->setOutputChannelMode(KProcess::MergedChannels);
connect(m_process, QOverload<int, QProcess::ExitStatus>::of(&KProcess::finished),
this, &ScratchpadJob::processFinished);
connect(m_process, &KProcess::errorOccurred, this, &ScratchpadJob::processError);
} else {
qCCritical(PLUGIN_SCRATCHPAD) << "Empty command in scratch job.";
deleteLater();
}
}
void ScratchpadJob::start()
{
const auto program = m_process->program().join(QLatin1Char(' '));
if (!program.trimmed().isEmpty()) {
startOutput();
outputModel()->appendLine(i18n("Running %1...", program));
m_process->start();
}
}
bool ScratchpadJob::doKill()
{
qCDebug(PLUGIN_SCRATCHPAD) << "killing process";
m_process->kill();
return true;
}
void ScratchpadJob::processFinished(int exitCode, QProcess::ExitStatus)
{
qCDebug(PLUGIN_SCRATCHPAD) << "finished process";
m_lineMaker->flushBuffers();
outputModel()->appendLine(i18n("Process finished with exit code %1.", exitCode));
emitResult();
}
void ScratchpadJob::processError(QProcess::ProcessError error)
{
qCDebug(PLUGIN_SCRATCHPAD) << "process encountered error" << error;
outputModel()->appendLine(i18n("Failed to run scratch: %1",
QLatin1String(QMetaEnum::fromType<QProcess::ProcessError>().valueToKey(error))));
emitResult();
}
KDevelop::OutputModel* ScratchpadJob::outputModel() const
{
return static_cast<KDevelop::OutputModel*>(model());
}
/* This file is part of KDevelop
*
* Copyright 2018 Amish K. Naidu <amhndu@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#ifndef SCRATCHPADJOB_H
#define SCRATCHPADJOB_H
#include <outputview/outputjob.h>
#include <QProcess>
namespace KDevelop {
class OutputModel;
class ProcessLineMaker;
}
class KProcess;
class ScratchpadJob
: public KDevelop::OutputJob
{
Q_OBJECT
public:
ScratchpadJob(const QString& command, const QString& title, QObject* parent);
void start() override;
bool doKill() override;
private Q_SLOTS:
void processFinished(int exitCode, QProcess::ExitStatus status);
void processError(QProcess::ProcessError error);
private:
KDevelop::OutputModel* outputModel() const;
KProcess* m_process;
KDevelop::ProcessLineMaker* m_lineMaker;
};
#endif // SCRATCHPADJOB_H
/* This file is part of KDevelop
*
* Copyright 2018 Amish K. Naidu <amhndu@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
#include "scratchpadview.h"
#include "scratchpad.h"
#include <debug.h>
#include <interfaces/icore.h>
#include <interfaces/idocumentcontroller.h>
#include <interfaces/idocument.h>
#include <KLocalizedString>
#include <KMessageBox>
#include <QAction>
#include <QStandardItemModel>
#include <QSortFilterProxyModel>
#include <QStyledItemDelegate>
#include <QWidgetAction>
#include <QLineEdit>
#include <QInputDialog>
#include <QPainter>
// Use a delegate because the dataChanged signal doesn't tell us the previous name
class FileRenameDelegate
: public QStyledItemDelegate
{
public:
FileRenameDelegate(QObject* parent, Scratchpad* scratchpad)
: QStyledItemDelegate(parent)
, m_scratchpad(scratchpad)
{
}
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override
{
const QString previousName = index.data().toString();
QStyledItemDelegate::setModelData(editor, model, index);
const auto* proxyModel = static_cast<QAbstractProxyModel*>(model);
m_scratchpad->renameScratch(proxyModel->mapToSource(index), previousName);
}
private:
Scratchpad* m_scratchpad;
};
EmptyMessageListView::EmptyMessageListView(QWidget* parent)
: QListView(parent)
{
}
void EmptyMessageListView::paintEvent(QPaintEvent* event)
{
if (model() && model()->rowCount(rootIndex()) > 0) {
QListView::paintEvent(event);
} else {
QPainter painter(viewport());
const auto margin =
QMargins(parentWidget()->style()->pixelMetric(QStyle::PM_LayoutLeftMargin), 0,
parentWidget()->style()->pixelMetric(QStyle::PM_LayoutRightMargin), 0);
painter.drawText(rect() - margin, Qt::AlignCenter | Qt::TextWordWrap, m_message);
}
}
void EmptyMessageListView::setEmptyMessage(const QString& message)
{
m_message = message;
}
ScratchpadView::ScratchpadView(QWidget* parent, Scratchpad* scratchpad)
: QWidget(parent)
, m_scratchpad(scratchpad)
{
setupUi(this);
setupActions();
setWindowTitle(i18n("Scratchpad"));
setWindowIcon(QIcon::fromTheme(QStringLiteral("note")));
auto* const modelProxy = new QSortFilterProxyModel(this);
modelProxy->setSourceModel(m_scratchpad->model());
modelProxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
modelProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
modelProxy->setSortRole(Qt::DisplayRole);
connect(m_filter, &QLineEdit::textEdited,
modelProxy, &QSortFilterProxyModel::setFilterWildcard);
scratchView->setModel(modelProxy);
scratchView->setItemDelegate(new FileRenameDelegate(this, m_scratchpad));
scratchView->setEmptyMessage(i18n("Scratchpad lets you quickly run and experiment with code without a full project, and even store todos. Create a new scratch to start."));
connect(scratchView, &QListView::activated, this, &ScratchpadView::scratchActivated);
connect(m_scratchpad, &Scratchpad::actionFailed, [this](const QString& message) {
KMessageBox::sorry(this, message);
});
connect(commandWidget, &QLineEdit::returnPressed, this, &ScratchpadView::runSelectedScratch);
connect(commandWidget, &QLineEdit::returnPressed, [this] {
m_scratchpad->setCommand(proxyModel()->mapToSource(currentIndex()), commandWidget->text());
});
commandWidget->setToolTip(i18n("Command to run this scratch. $f will expand to the scratch path"));
commandWidget->setPlaceholderText(commandWidget->toolTip());
// change active scratch when changing document
connect(KDevelop::ICore::self()->documentController(), &KDevelop::IDocumentController::documentActivated,
[this](const KDevelop::IDocument* document) {
if (document->url().isLocalFile()) {
const auto* model = scratchView->model();