kate-deft/src/sam/samplugin.cpp

223 lines
7.5 KiB
C++

/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "samplugin.h"
#include "samengine.h"
#include "sampanel.h"
#include <KTextEditor/Application>
#include <KTextEditor/Document>
#include <KTextEditor/Editor>
#include <KTextEditor/MainWindow>
#include <KTextEditor/View>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QIcon>
#include <QRegularExpression>
#include <QVBoxLayout>
#include <QWidget>
namespace deft
{
// ---------------------------------------------------------------------------
// SamPlugin
// ---------------------------------------------------------------------------
SamPlugin::SamPlugin(QObject *parent, const QVariantList &args)
: KTextEditor::Plugin(parent)
{
Q_UNUSED(args);
}
SamPlugin::~SamPlugin() = default;
QObject *SamPlugin::createView(KTextEditor::MainWindow *mainWindow)
{
return new SamView(this, mainWindow);
}
// ---------------------------------------------------------------------------
// SamView
// ---------------------------------------------------------------------------
SamView::SamView(SamPlugin *plugin, KTextEditor::MainWindow *mainWindow)
: QObject(plugin)
, m_mainWindow(mainWindow)
{
// sam tool view: a dockable panel to run sam structural-regexp commands
// against the active document (or project-wide via X/Y). Created on the
// left sidebar; the host owns the returned widget.
m_samToolView = m_mainWindow->createToolView(
plugin, QStringLiteral("deft_sam"), KTextEditor::MainWindow::Left,
QIcon::fromTheme(QStringLiteral("edit-find-replace")), i18n("Sam"));
if (m_samToolView) {
m_samPanel = new SamPanel(m_samToolView);
// The tool view returned by Kate (KateMDI::ToolView) already installs
// its own layout; creating a second one here is silently rejected by Qt
// and leaves SamPanel un-managed (tiny, pinned top-left). Add to the
// existing layout when there is one, otherwise create it ourselves.
QLayout *tvLayout = m_samToolView->layout();
if (!tvLayout) {
tvLayout = new QVBoxLayout(m_samToolView);
tvLayout->setContentsMargins(0, 0, 0, 0);
}
tvLayout->addWidget(m_samPanel);
connect(m_samPanel, &SamPanel::runRequested, this, &SamView::runSamProgram);
}
}
SamView::~SamView()
{
delete m_samToolView; // deletes the embedded SamPanel with it
}
bool SamView::applySamToDocument(KTextEditor::Document *doc, const QString &program,
int dotStart, int dotEnd, QString *output, QString *error)
{
const QString text = doc->text();
const SamResult res = SamEngine::run(program, text, dotStart, dotEnd);
if (!res.ok) {
if (error) {
*error = res.error;
}
return false;
}
if (output) {
*output = res.output;
}
// Apply all edits inside one transaction so a single undo reverts the whole
// Run. Edits are in ORIGINAL-text offsets, non-overlapping and sorted by
// start; apply back-to-front so earlier offsets remain valid.
if (!res.edits.isEmpty()) {
KTextEditor::Document::EditingTransaction transaction(doc);
for (int i = res.edits.size() - 1; i >= 0; i--) {
const SamEdit &e = res.edits[i];
const KTextEditor::Cursor from = doc->offsetToCursor(e.start);
const KTextEditor::Cursor to = doc->offsetToCursor(e.end);
doc->replaceText(KTextEditor::Range(from, to), e.text);
}
}
return true;
}
void SamView::runSamProgram(const QString &program)
{
if (!m_samPanel) {
return;
}
const QString trimmed = program.trimmed();
if (trimmed.isEmpty()) {
return;
}
// Multi-file X/Y: drive the sweep over the project's files.
const SamEngine::FileLoop fl = SamEngine::peelFileLoop(trimmed);
if (fl.matched) {
runSamFileLoop(fl.negate, fl.fileRegex, fl.inner);
return;
}
// Single-file: run against the active document.
KTextEditor::View *view = m_mainWindow->activeView();
if (!view || !view->document()) {
m_samPanel->showError(i18n("No active document."));
return;
}
KTextEditor::Document *doc = view->document();
// Dot = current selection, else the whole document (sam's common idioms use
// explicit ranges like `,` anyway; whole-doc is a sensible default dot).
int dotStart = 0;
int dotEnd = doc->text().size();
if (view->selection()) {
const KTextEditor::Range sel = view->selectionRange();
dotStart = int(doc->cursorToOffset(sel.start()));
dotEnd = int(doc->cursorToOffset(sel.end()));
}
QString output;
QString error;
if (!applySamToDocument(doc, program, dotStart, dotEnd, &output, &error)) {
m_samPanel->showError(error);
return;
}
if (!output.isEmpty()) {
m_samPanel->showOutput(output);
} else {
m_samPanel->showOutput(i18n("(done)"));
}
}
void SamView::runSamFileLoop(bool negate, const QString &fileRegex, const QString &inner)
{
if (!m_samPanel) {
return;
}
if (inner.trimmed().isEmpty()) {
m_samPanel->showError(i18n("X/Y: no command to run on matched buffers."));
return;
}
QRegularExpression fileRe(fileRegex);
if (!fileRe.isValid()) {
m_samPanel->showError(i18n("X/Y: bad file regexp: %1", fileRe.errorString()));
return;
}
// X/Y iterate the set of OPEN buffers (like acme's open-window set), not the
// whole project on disk. Nothing is opened or read from disk: we edit the
// documents Kate already has loaded, in place, each as its own undo step.
KTextEditor::Editor *editor = KTextEditor::Editor::instance();
KTextEditor::Application *app = editor ? editor->application() : nullptr;
const QList<KTextEditor::Document *> docs =
app ? app->documents() : QList<KTextEditor::Document *>{};
if (docs.isEmpty()) {
m_samPanel->showError(i18n("X/Y: no open buffers."));
return;
}
int matched = 0;
int changed = 0;
for (KTextEditor::Document *doc : docs) {
if (!doc) {
continue;
}
// Match the regex against the buffer's path (its local file path, or its
// display name for an unsaved scratch buffer).
const QUrl url = doc->url();
const QString target =
url.isLocalFile() ? url.toLocalFile() : doc->documentName();
const bool hit = fileRe.match(target).hasMatch();
if (hit == negate) {
continue; // X runs on hits; Y runs on misses
}
matched++;
QString output;
QString error;
const int dotEnd = doc->text().size();
if (applySamToDocument(doc, inner, 0, dotEnd, &output, &error)) {
changed++;
if (!output.isEmpty()) {
m_samPanel->showOutput(QStringLiteral("%1:\n%2").arg(target, output));
}
} else {
// A "no match" in a given buffer is normal during a sweep; report it
// quietly rather than as a hard error.
m_samPanel->showOutput(QStringLiteral("%1: %2").arg(target, error));
}
}
m_samPanel->showOutput(
i18n("X/Y done: %1 open buffer(s) matched, %2 changed.", matched, changed));
}
} // namespace deft
// K_PLUGIN_FACTORY generates the factory and the moc-level registration. The
// matching .moc include is required in this translation unit.
K_PLUGIN_FACTORY_WITH_JSON(SamPluginFactory, "samplugin.json",
registerPlugin<deft::SamPlugin>();)
#include "samplugin.moc"