/* * SPDX-License-Identifier: LGPL-2.0-or-later */ #include "samplugin.h" #include "samengine.h" #include "sampanel.h" #include #include #include #include #include #include #include #include #include #include #include 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 docs = app ? app->documents() : QList{}; 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();) #include "samplugin.moc"