diff --git a/src/plugin/CMakeLists.txt b/src/plugin/CMakeLists.txt index c030b88..5c70d2a 100644 --- a/src/plugin/CMakeLists.txt +++ b/src/plugin/CMakeLists.txt @@ -24,7 +24,7 @@ if(PkgConfig_FOUND) pkg_check_modules(WAYLAND_PROTOCOLS QUIET wayland-protocols) endif() -set(OLLIE_PLUGIN_SOURCES ollieplugin.cpp ollieplugin.h sampanel.cpp sampanel.h waylandcursorwarp.h) +set(OLLIE_PLUGIN_SOURCES ollieplugin.cpp ollieplugin.h waylandcursorwarp.h) set(OLLIE_WAYLAND_PROTO_SRCS "") if(WaylandScanner_FOUND AND WAYLAND_CLIENT_FOUND AND Qt6GuiPrivate_FOUND) @@ -73,7 +73,6 @@ target_link_libraries(deft PRIVATE olliecommands_lib project_lib plumb_lib - sam_lib KF6::TextEditor KF6::XmlGui KF6::CoreAddons diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index 9b01d95..f3ff049 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -11,8 +11,6 @@ #include "projectindex.h" #include "symbolindex.h" #include "plumbresolve.h" -#include "sampanel.h" -#include "samengine.h" #include #include @@ -47,7 +45,6 @@ #include #include #include -#include #include #include @@ -417,27 +414,6 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow) }); m_plumber.startReader(); - // 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("ollie_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, &OllieView::runSamProgram); - } - // Build on Kate's project plugin: attach to its view now (if present) and // follow it as plugins come and go. The bridge is read-only; the switchers // scope to its project when one is loaded, else to VCS discovery. @@ -476,7 +452,6 @@ OllieView::~OllieView() delete m_filePalette; delete m_symbolPalette; delete m_radial; - delete m_samToolView; // deletes the embedded SamPanel with it } QList OllieView::collectActions() const @@ -1045,145 +1020,6 @@ void OllieView::onPlumbEdit(const QString &file, const QString &addr, const QStr } } -bool OllieView::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 OllieView::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 OllieView::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)); -} - QAction *OllieView::findActionByName(const QString &nameOrText) const { KXMLGUIFactory *factory = m_mainWindow->guiFactory(); diff --git a/src/plugin/ollieplugin.h b/src/plugin/ollieplugin.h index aee62b1..0bd3ea8 100644 --- a/src/plugin/ollieplugin.h +++ b/src/plugin/ollieplugin.h @@ -18,7 +18,6 @@ #include "projectindexer.h" #include "palettemodel.h" // PaletteItem #include "plumber.h" -#include "sampanel.h" #include "waylandcursorwarp.h" #include @@ -81,10 +80,6 @@ private Q_SLOTS: // addr (a plan9 edit-port address). Routed from the Plumber reader thread. void onPlumbEdit(const QString &file, const QString &addr, const QString &wdir); - // Run a sam program from the panel. Dispatches to the multi-file X/Y driver - // when the program begins with X or Y, else runs against the active doc. - void runSamProgram(const QString &program); - private: // Record one activation of \a id and persist. Feeds the frecency bonus so // habitual choices float up in every palette. @@ -149,16 +144,6 @@ private: // matching pair (multi-line aware). Returns true if a pair was selected. bool selectBracketPairAt(KTextEditor::View *view, const QPoint &globalPos); - // Apply a sam program to a single document, returning the engine result. - // Edits are applied inside one EditingTransaction (a single undo step). - // \a doc must be non-null. Does not touch the panel. - bool applySamToDocument(KTextEditor::Document *doc, const QString &program, - int dotStart, int dotEnd, QString *output, QString *error); - - // Drive a multi-file X/Y sweep over the project's files, opening each match - // and applying \a inner. Reports progress/errors to the panel. - void runSamFileLoop(bool negate, const QString &fileRegex, const QString &inner); - KTextEditor::MainWindow *m_mainWindow; PaletteWidget *m_palette = nullptr; PaletteWidget *m_filePalette = nullptr; // project "Go to File" switcher @@ -178,8 +163,6 @@ private: ProjectIndexer m_indexer; // async, cached file/symbol indexing WaylandCursorWarp m_cursorWarp; // pointer warp on Wayland (no-op on X11) Plumber m_plumber; // plan9 plumbing client (send + edit-port reader) - SamPanel *m_samPanel = nullptr; // sam command tool view (owned by the host) - QWidget *m_samToolView = nullptr; // the dockable container from createToolView }; } // namespace deft diff --git a/src/sam/CMakeLists.txt b/src/sam/CMakeLists.txt index 6b7c70a..454dc8a 100644 --- a/src/sam/CMakeLists.txt +++ b/src/sam/CMakeLists.txt @@ -24,3 +24,20 @@ if(Qt6Test_FOUND) target_link_libraries(test_samregex PRIVATE sam_lib Qt6::Test) add_test(NAME samregex COMMAND test_samregex) endif() + +# --- [deft:nineify] sam — the Kate plugin ---------------------------------- +# The sam engine (sam_lib) is pure and always built. The plugin layer (tool +# view + active-document glue) needs KF6::TextEditor; build it only when found. +if(KF6TextEditor_FOUND) + kcoreaddons_add_plugin(deft_sam + SOURCES samplugin.cpp samplugin.h sampanel.cpp sampanel.h + INSTALL_NAMESPACE "kf6/ktexteditor" + ) + target_link_libraries(deft_sam PRIVATE + sam_lib + KF6::TextEditor + KF6::CoreAddons + KF6::I18n + Qt6::Widgets + ) +endif() diff --git a/src/plugin/sampanel.cpp b/src/sam/sampanel.cpp similarity index 100% rename from src/plugin/sampanel.cpp rename to src/sam/sampanel.cpp diff --git a/src/plugin/sampanel.h b/src/sam/sampanel.h similarity index 100% rename from src/plugin/sampanel.h rename to src/sam/sampanel.h diff --git a/src/sam/samplugin.cpp b/src/sam/samplugin.cpp new file mode 100644 index 0000000..18cc07e --- /dev/null +++ b/src/sam/samplugin.cpp @@ -0,0 +1,222 @@ +/* + * 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" diff --git a/src/sam/samplugin.h b/src/sam/samplugin.h new file mode 100644 index 0000000..f577722 --- /dev/null +++ b/src/sam/samplugin.h @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: LGPL-2.0-or-later + * + * [deft:nineify] sam — the structural-regexp command plugin for Kate. + * + * SamPlugin is the KTextEditor::Plugin entry point. For every MainWindow it + * creates a SamView, which installs a dockable "Sam" tool view (a SamPanel) and + * runs sam programs against the active document, or project-wide via the X/Y + * multi-file loop. The sam engine (sam_lib) is pure; this layer is only the + * Kate glue (tool view, active document, EditingTransaction). + */ +#ifndef DEFT_SAMPLUGIN_H +#define DEFT_SAMPLUGIN_H + +#include + +#include "sampanel.h" + +#include +#include + +namespace KTextEditor +{ +class Document; +class MainWindow; +} + +namespace deft +{ + +class SamPlugin : public KTextEditor::Plugin +{ + Q_OBJECT + +public: + explicit SamPlugin(QObject *parent, const QVariantList &args = {}); + ~SamPlugin() override; + + QObject *createView(KTextEditor::MainWindow *mainWindow) override; +}; + +class SamView : public QObject +{ + Q_OBJECT + +public: + SamView(SamPlugin *plugin, KTextEditor::MainWindow *mainWindow); + ~SamView() override; + +private Q_SLOTS: + // Run a sam program from the panel. Dispatches to the multi-file X/Y driver + // when the program begins with X or Y, else runs against the active doc. + void runSamProgram(const QString &program); + +private: + // Apply a sam program to a single document, returning the engine result. + // Edits are applied inside one EditingTransaction (a single undo step). + // \a doc must be non-null. Does not touch the panel. + bool applySamToDocument(KTextEditor::Document *doc, const QString &program, + int dotStart, int dotEnd, QString *output, QString *error); + + // Drive a multi-file X/Y sweep over the open buffers, applying \a inner. + // Reports progress/errors to the panel. + void runSamFileLoop(bool negate, const QString &fileRegex, const QString &inner); + + KTextEditor::MainWindow *m_mainWindow; + SamPanel *m_samPanel = nullptr; // sam command tool view (owned by host) + QWidget *m_samToolView = nullptr; // the dockable container from createToolView +}; + +} // namespace deft + +#endif diff --git a/src/sam/samplugin.json b/src/sam/samplugin.json new file mode 100644 index 0000000..6adcf4a --- /dev/null +++ b/src/sam/samplugin.json @@ -0,0 +1,14 @@ +{ + "KPlugin": { + "Authors": [ + { + "Name": "Levi Neely" + } + ], + "Description": "Run plan9 sam structural-regexp commands against the active document or across open buffers (X/Y).", + "Icon": "edit-find-replace", + "License": "LGPL-2.0-or-later", + "Name": "[deft:nineify] sam", + "Version": "0.1.0" + } +}