diff --git a/docs/PLAN.md b/docs/PLAN.md index 7f916b1..77b4611 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -382,13 +382,15 @@ program, click **Run** (or Ctrl+Return); each Run is one undo step. loops `x y g v` (nestable, `{}` groups), and shell filters `< > | !` via `sh -c`. 27 unit tests cover each. - **Multi-file `X`/`Y`** (`SamEngine::peelFileLoop` + Kate driver): a leading - `X/re/ cmd` runs `cmd` on every project file whose path matches `re` (`Y` = - non-match). The file set is the project index (`ProjectIndexer::cachedFiles`); - each matched file is opened as a Document and edited. sam permits only one + `X/re/ cmd` runs `cmd` on every **open buffer** whose path matches `re` (`Y` = + non-match). The set is the documents Kate currently has open + (`Application::documents()`) — acme's open-window set — matched by local file + path, or display name for an unsaved scratch buffer. Nothing is opened or read + from disk; the live `Document`s are edited in place. sam permits only one `X`/`Y` per command, so this is leading-token recognition, not a nested parse. - **Undo**: single-document Run applies all edits inside one `Document::EditingTransaction` → one Ctrl+Z reverts the whole Run. Multi-file - `X`/`Y` is undoable **per file** (KTextEditor has no global multi-file undo). + `X`/`Y` is undoable **per buffer** (KTextEditor has no global multi-file undo). - **Panel** (`src/plugin/sampanel.{h,cpp}`): program editor + Run button + output log; `OllieView` creates the tool view and owns the apply logic (`runSamProgram`, `applySamToDocument`, `runSamFileLoop`). diff --git a/docs/SAM.md b/docs/SAM.md index aec51a1..7c099e6 100644 --- a/docs/SAM.md +++ b/docs/SAM.md @@ -80,15 +80,18 @@ matches with `x`/`y`, guard with `g`/`v`, group with `{}`: 0/start/,/end/ x/[a-z]+/ s/.*/[&]/ bracket every lowercase word in a region ``` -Multi-file `X`/`Y` applies an inner program across the project: +Multi-file `X`/`Y` applies an inner program across the **open buffers**: ``` -X/\.cpp$/ ,s/old_api/new_api/g rewrite every .cpp in the project -Y/_test\./ ,x/TODO/ d drop TODO lines in non-test files +X/\.cpp$/ ,s/old_api/new_api/g rewrite every open .cpp buffer +Y/_test\./ ,x/TODO/ d drop TODO lines in non-test buffers ``` -(The file set is the project index, matched by path. Edits land in buffers, not -on disk — review and save deliberately. Undo is per file for `X`/`Y`.) +(`X`/`Y` iterate the set of documents Kate currently has open — acme's +open-window set — not the project on disk. The regex matches each buffer's file +path, or its display name for an unsaved scratch buffer. Nothing is opened or +read from disk; edits land in the live buffers, each its own undo step. Review +and save deliberately.) ## Practical guidance diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index f1050c5..a1dd747 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -14,11 +14,12 @@ #include "sampanel.h" #include "samengine.h" -#include -#include +#include +#include #include #include -#include +#include +#include #include #include @@ -990,13 +991,8 @@ void OllieView::runSamFileLoop(bool negate, const QString &fileRegex, const QStr if (!m_samPanel) { return; } - const QString root = currentProjectRoot(); - if (root.isEmpty()) { - m_samPanel->showError(i18n("X/Y needs a project; none is loaded.")); - return; - } if (inner.trimmed().isEmpty()) { - m_samPanel->showError(i18n("X/Y: no command to run on matched files.")); + m_samPanel->showError(i18n("X/Y: no command to run on matched buffers.")); return; } QRegularExpression fileRe(fileRegex); @@ -1005,50 +1001,50 @@ void OllieView::runSamFileLoop(bool negate, const QString &fileRegex, const QStr return; } - // Use the cached project file list; request a build if we have none yet. - if (!m_indexer.hasFiles(root)) { - m_samPanel->showOutput(i18n("Indexing project files… run X/Y again in a moment.")); - m_indexer.requestFiles(root); + // 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; } - const QStringList files = m_indexer.cachedFiles(root); int matched = 0; int changed = 0; - int failed = 0; - for (const QString &rel : files) { - const bool hit = fileRe.match(rel).hasMatch(); + 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++; - const QString absolute = QDir(root).absoluteFilePath(rel); - const QUrl url = QUrl::fromLocalFile(absolute); - // Open (or focus) the file so the edit is a normal, undoable Document - // change visible to the user. - KTextEditor::View *view = m_mainWindow->openUrl(url); - if (!view || !view->document()) { - failed++; - m_samPanel->showError(i18n("could not open %1", rel)); - continue; - } - KTextEditor::Document *doc = view->document(); 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(rel, output)); + m_samPanel->showOutput(QStringLiteral("%1:\n%2").arg(target, output)); } } else { - // A "no match" in a given file is normal during a sweep; report it + // 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(rel, error)); + m_samPanel->showOutput(QStringLiteral("%1: %2").arg(target, error)); } } m_samPanel->showOutput( - i18n("X/Y done: %1 file(s) matched, %2 changed, %3 failed.", matched, changed, failed)); + i18n("X/Y done: %1 open buffer(s) matched, %2 changed.", matched, changed)); } QAction *OllieView::findActionByName(const QString &nameOrText) const