sam X/Y: target open buffers, not the whole project
Scope X/Y to the set of documents Kate currently has open (Application::documents()) — acme's open-window set — instead of the project index on disk. The regex matches each buffer's local file path (or display name for an unsaved scratch buffer). Nothing is opened or read from disk; the live Documents are edited in place, each its own undo step. Smaller blast radius than a project-wide sweep and closer to acme's X/Y, which iterate open windows. Drops the ProjectIndexer dependency from the X/Y path. Docs (SAM.md, PLAN.md) updated. 18/18 ctest green.
This commit is contained in:
parent
b9a539d99c
commit
df287b3a7f
10
docs/PLAN.md
10
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`).
|
||||
|
|
|
|||
13
docs/SAM.md
13
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
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@
|
|||
#include "sampanel.h"
|
||||
#include "samengine.h"
|
||||
|
||||
#include <KTextEditor/MainWindow>
|
||||
#include <KTextEditor/View>
|
||||
#include <KTextEditor/Application>
|
||||
#include <KTextEditor/Command>
|
||||
#include <KTextEditor/Document>
|
||||
#include <KTextEditor/Editor>
|
||||
#include <KTextEditor/Command>
|
||||
#include <KTextEditor/MainWindow>
|
||||
#include <KTextEditor/View>
|
||||
|
||||
#include <KActionCollection>
|
||||
#include <KXMLGUIClient>
|
||||
|
|
@ -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<KTextEditor::Document *> docs =
|
||||
app ? app->documents() : QList<KTextEditor::Document *>{};
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue