sam: extract into standalone [deft:nineify] sam plugin (deft_sam.so)
Stage 2a of the multi-.so split. sam becomes its own KTextEditor plugin:
new src/sam/samplugin.{h,cpp} (SamPlugin/SamView) + samplugin.json
("[deft:nineify] sam"), built as deft_sam.so. The sam tool-view wiring and
the runSamProgram/applySamToDocument/runSamFileLoop glue move out of
OllieView into SamView; sampanel.{h,cpp} move from src/plugin to src/sam.
The core deft plugin no longer links sam_lib or owns the sam panel.
This commit is contained in:
parent
d96cb63baf
commit
debe1226f7
|
|
@ -24,7 +24,7 @@ if(PkgConfig_FOUND)
|
||||||
pkg_check_modules(WAYLAND_PROTOCOLS QUIET wayland-protocols)
|
pkg_check_modules(WAYLAND_PROTOCOLS QUIET wayland-protocols)
|
||||||
endif()
|
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 "")
|
set(OLLIE_WAYLAND_PROTO_SRCS "")
|
||||||
|
|
||||||
if(WaylandScanner_FOUND AND WAYLAND_CLIENT_FOUND AND Qt6GuiPrivate_FOUND)
|
if(WaylandScanner_FOUND AND WAYLAND_CLIENT_FOUND AND Qt6GuiPrivate_FOUND)
|
||||||
|
|
@ -73,7 +73,6 @@ target_link_libraries(deft PRIVATE
|
||||||
olliecommands_lib
|
olliecommands_lib
|
||||||
project_lib
|
project_lib
|
||||||
plumb_lib
|
plumb_lib
|
||||||
sam_lib
|
|
||||||
KF6::TextEditor
|
KF6::TextEditor
|
||||||
KF6::XmlGui
|
KF6::XmlGui
|
||||||
KF6::CoreAddons
|
KF6::CoreAddons
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,6 @@
|
||||||
#include "projectindex.h"
|
#include "projectindex.h"
|
||||||
#include "symbolindex.h"
|
#include "symbolindex.h"
|
||||||
#include "plumbresolve.h"
|
#include "plumbresolve.h"
|
||||||
#include "sampanel.h"
|
|
||||||
#include "samengine.h"
|
|
||||||
|
|
||||||
#include <KTextEditor/Application>
|
#include <KTextEditor/Application>
|
||||||
#include <KTextEditor/Command>
|
#include <KTextEditor/Command>
|
||||||
|
|
@ -47,7 +45,6 @@
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QVBoxLayout>
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
|
|
@ -417,27 +414,6 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
||||||
});
|
});
|
||||||
m_plumber.startReader();
|
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
|
// 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
|
// 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.
|
// scope to its project when one is loaded, else to VCS discovery.
|
||||||
|
|
@ -476,7 +452,6 @@ OllieView::~OllieView()
|
||||||
delete m_filePalette;
|
delete m_filePalette;
|
||||||
delete m_symbolPalette;
|
delete m_symbolPalette;
|
||||||
delete m_radial;
|
delete m_radial;
|
||||||
delete m_samToolView; // deletes the embedded SamPanel with it
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<OllieView::ActionEntry> OllieView::collectActions() const
|
QList<OllieView::ActionEntry> 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<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));
|
|
||||||
}
|
|
||||||
|
|
||||||
QAction *OllieView::findActionByName(const QString &nameOrText) const
|
QAction *OllieView::findActionByName(const QString &nameOrText) const
|
||||||
{
|
{
|
||||||
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
|
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@
|
||||||
#include "projectindexer.h"
|
#include "projectindexer.h"
|
||||||
#include "palettemodel.h" // PaletteItem
|
#include "palettemodel.h" // PaletteItem
|
||||||
#include "plumber.h"
|
#include "plumber.h"
|
||||||
#include "sampanel.h"
|
|
||||||
#include "waylandcursorwarp.h"
|
#include "waylandcursorwarp.h"
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
@ -81,10 +80,6 @@ private Q_SLOTS:
|
||||||
// addr (a plan9 edit-port address). Routed from the Plumber reader thread.
|
// addr (a plan9 edit-port address). Routed from the Plumber reader thread.
|
||||||
void onPlumbEdit(const QString &file, const QString &addr, const QString &wdir);
|
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:
|
private:
|
||||||
// Record one activation of \a id and persist. Feeds the frecency bonus so
|
// Record one activation of \a id and persist. Feeds the frecency bonus so
|
||||||
// habitual choices float up in every palette.
|
// habitual choices float up in every palette.
|
||||||
|
|
@ -149,16 +144,6 @@ private:
|
||||||
// matching pair (multi-line aware). Returns true if a pair was selected.
|
// matching pair (multi-line aware). Returns true if a pair was selected.
|
||||||
bool selectBracketPairAt(KTextEditor::View *view, const QPoint &globalPos);
|
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;
|
KTextEditor::MainWindow *m_mainWindow;
|
||||||
PaletteWidget *m_palette = nullptr;
|
PaletteWidget *m_palette = nullptr;
|
||||||
PaletteWidget *m_filePalette = nullptr; // project "Go to File" switcher
|
PaletteWidget *m_filePalette = nullptr; // project "Go to File" switcher
|
||||||
|
|
@ -178,8 +163,6 @@ private:
|
||||||
ProjectIndexer m_indexer; // async, cached file/symbol indexing
|
ProjectIndexer m_indexer; // async, cached file/symbol indexing
|
||||||
WaylandCursorWarp m_cursorWarp; // pointer warp on Wayland (no-op on X11)
|
WaylandCursorWarp m_cursorWarp; // pointer warp on Wayland (no-op on X11)
|
||||||
Plumber m_plumber; // plan9 plumbing client (send + edit-port reader)
|
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
|
} // namespace deft
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,20 @@ if(Qt6Test_FOUND)
|
||||||
target_link_libraries(test_samregex PRIVATE sam_lib Qt6::Test)
|
target_link_libraries(test_samregex PRIVATE sam_lib Qt6::Test)
|
||||||
add_test(NAME samregex COMMAND test_samregex)
|
add_test(NAME samregex COMMAND test_samregex)
|
||||||
endif()
|
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()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
|
@ -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 <KTextEditor/Plugin>
|
||||||
|
|
||||||
|
#include "sampanel.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue