223 lines
7.5 KiB
C++
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"
|