M5: project = directory + Go-to-File switcher (Alt+P)
ProjectIndex (project_lib): findRoot walks up for a VCS marker and falls back to the start dir; listFiles prefers git ls-files (--cached --others --exclude-standard) and falls back to a bounded recursive walk skipping noise dirs/symlinks. 12 unit tests. Plugin: Alt+P 'Go to File (project)' opens a second PaletteWidget of the project files, scoped to the active document's root (fallback cwd), and opens the pick via MainWindow::openUrl+activateView. Also surfaced in the M-x palette and reachable from a radial by objectName ollie_goto_file.
This commit is contained in:
parent
a69e471e08
commit
f4b891efa7
20
docs/PLAN.md
20
docs/PLAN.md
|
|
@ -186,8 +186,24 @@ Therefore the palette is replaced, not extended, and the replacement is built on
|
|||
constructor is NOT visible to `Editor::queryCommand()`/`commandList()` on
|
||||
this build, so the verbs are not (yet) reachable from Kate's `:` command
|
||||
line — only via the palette. Proper command-line registration is a follow-up.
|
||||
- Project = directory ("open folder == open project"): TODO.
|
||||
- Fuzzy file / symbol switchers scoped to the folder: TODO.
|
||||
- Project = directory ("open folder == open project"): DONE (core + switcher).
|
||||
`src/project/projectindex.{h,cpp}` (`project_lib`, Qt::Core only, 12 unit
|
||||
tests):
|
||||
- `findRoot(startDir)` walks up for a VCS marker (`.git`/`.hg`/`.svn`) and
|
||||
returns that ancestor, else the start directory itself — so any folder is a
|
||||
usable project with zero config.
|
||||
- `listFiles(root)` prefers `git ls-files --cached --others
|
||||
--exclude-standard` inside a git work tree (honours .gitignore, includes
|
||||
untracked-not-ignored), and falls back to a bounded recursive walk
|
||||
(`walkDirectory`) that skips noise dirs (node_modules, build, target, …) and
|
||||
symlinks when there is no git (or git yields nothing).
|
||||
- Wired into the plugin: **Alt+P** "Go to File (project)" opens a second
|
||||
PaletteWidget populated with the project's files (base name as the label,
|
||||
directory as the trailing hint). The root is derived from the active
|
||||
document's directory (fallback: cwd). Activation opens the file via
|
||||
`MainWindow::openUrl` + `activateView`. Also reachable from the M-x palette
|
||||
(`ollie:switch:file`) and, by objectName `ollie_goto_file`, from a radial.
|
||||
- Fuzzy symbol switcher scoped to the folder: TODO (ctags/LSP; ctags present).
|
||||
|
||||
## Build and test
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
add_subdirectory(fuzzy)
|
||||
add_subdirectory(palette)
|
||||
add_subdirectory(radial)
|
||||
add_subdirectory(project)
|
||||
if(KF6TextEditor_FOUND)
|
||||
add_subdirectory(commands)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ target_link_libraries(olliepalette PRIVATE
|
|||
palette
|
||||
radial
|
||||
olliecommands_lib
|
||||
project_lib
|
||||
KF6::TextEditor
|
||||
KF6::XmlGui
|
||||
KF6::CoreAddons
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "radialmenu.h"
|
||||
#include "radialconfig.h"
|
||||
#include "olliecommands.h"
|
||||
#include "projectindex.h"
|
||||
|
||||
#include <KTextEditor/MainWindow>
|
||||
#include <KTextEditor/View>
|
||||
|
|
@ -22,12 +23,15 @@
|
|||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
#include <QKeyEvent>
|
||||
#include <QKeySequence>
|
||||
#include <QMouseEvent>
|
||||
#include <QSet>
|
||||
#include <QUrl>
|
||||
#include <QWidget>
|
||||
|
||||
namespace katecustom
|
||||
|
|
@ -128,6 +132,10 @@ KTextEditor::Cursor findOpener(KTextEditor::Document *doc, KTextEditor::Cursor p
|
|||
// than a QAction to trigger.
|
||||
const QLatin1String kCmdPrefix("ollie:cmd:");
|
||||
|
||||
// Prefix marking a palette id that is a project-relative file path to open in
|
||||
// the file switcher.
|
||||
const QLatin1String kFilePrefix("ollie:file:");
|
||||
|
||||
// The complete (argument-free) command verbs offered in the palette. Verbs that
|
||||
// need free-text arguments (pipe) stay on the command line; `case` is expanded
|
||||
// into one entry per style so each is directly runnable.
|
||||
|
|
@ -153,6 +161,26 @@ QList<VerbItem> paletteVerbs()
|
|||
{QStringLiteral("uuid"), QStringLiteral("Insert UUID")},
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve the project root for \a mainWindow: the discovered root of the active
|
||||
// document's directory, falling back to the process working directory when no
|
||||
// document is open or the document is untitled.
|
||||
QString projectRootFor(KTextEditor::MainWindow *mainWindow)
|
||||
{
|
||||
QString startDir;
|
||||
if (KTextEditor::View *view = mainWindow->activeView()) {
|
||||
if (KTextEditor::Document *doc = view->document()) {
|
||||
const QUrl url = doc->url();
|
||||
if (url.isLocalFile()) {
|
||||
startDir = QFileInfo(url.toLocalFile()).absolutePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (startDir.isEmpty()) {
|
||||
startDir = QDir::currentPath();
|
||||
}
|
||||
return ProjectIndex::findRoot(startDir);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -195,6 +223,19 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
|||
w->addAction(openAction);
|
||||
}
|
||||
|
||||
// Project "Go to File" switcher: Alt+P (single modifier; the Sublime/VSCode
|
||||
// Ctrl+P idiom adapted to one-handed use). Lists the project's files —
|
||||
// "open folder == open project" — scoped to the active document's git/root
|
||||
// directory.
|
||||
auto *gotoFileAction = new QAction(i18n("Go to File (project)"), this);
|
||||
gotoFileAction->setObjectName(QStringLiteral("ollie_goto_file"));
|
||||
gotoFileAction->setShortcut(QKeySequence(Qt::ALT | Qt::Key_P));
|
||||
gotoFileAction->setShortcutContext(Qt::ApplicationShortcut);
|
||||
connect(gotoFileAction, &QAction::triggered, this, &OllieView::showFileSwitcher);
|
||||
if (QWidget *w = m_mainWindow->window()) {
|
||||
w->addAction(gotoFileAction);
|
||||
}
|
||||
|
||||
setupRadials();
|
||||
|
||||
// One application-wide filter serves both the mouse-button radial gesture
|
||||
|
|
@ -206,6 +247,7 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
|||
OllieView::~OllieView()
|
||||
{
|
||||
delete m_palette;
|
||||
delete m_filePalette;
|
||||
delete m_radial;
|
||||
}
|
||||
|
||||
|
|
@ -360,6 +402,15 @@ void OllieView::showCommandPalette()
|
|||
items.push_back(item);
|
||||
}
|
||||
|
||||
// Offer the project file switcher as a palette entry too (also on Alt+P).
|
||||
{
|
||||
PaletteItem item;
|
||||
item.id = QStringLiteral("ollie:switch:file");
|
||||
item.label = i18n("Go to File (project)");
|
||||
item.group = QStringLiteral("Alt+P");
|
||||
items.push_back(item);
|
||||
}
|
||||
|
||||
m_palette->setItems(items);
|
||||
|
||||
// Center the palette near the top of the active window.
|
||||
|
|
@ -387,12 +438,74 @@ void OllieView::runAction(const QString &actionId)
|
|||
return;
|
||||
}
|
||||
|
||||
// The project file switcher, surfaced as a palette entry.
|
||||
if (actionId == QLatin1String("ollie:switch:file")) {
|
||||
showFileSwitcher();
|
||||
return;
|
||||
}
|
||||
|
||||
QPointer<QAction> action = m_actionsById.value(actionId);
|
||||
if (action) {
|
||||
action->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
void OllieView::showFileSwitcher()
|
||||
{
|
||||
if (!m_filePalette) {
|
||||
m_filePalette = new PaletteWidget(m_mainWindow->window());
|
||||
connect(m_filePalette, &PaletteWidget::activatedId, this,
|
||||
[this](const QString &id) {
|
||||
if (id.startsWith(kFilePrefix)) {
|
||||
openProjectFile(id.mid(QString(kFilePrefix).size()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
m_fileSwitcherRoot = projectRootFor(m_mainWindow);
|
||||
const QStringList files = ProjectIndex::listFiles(m_fileSwitcherRoot);
|
||||
|
||||
QList<PaletteItem> items;
|
||||
items.reserve(files.size());
|
||||
for (const QString &rel : files) {
|
||||
PaletteItem item;
|
||||
item.id = kFilePrefix + rel;
|
||||
// Show the file name as the label and the directory as the trailing
|
||||
// hint, so fuzzy matching favours the base name while the path stays
|
||||
// visible for disambiguation.
|
||||
const int slash = rel.lastIndexOf(QLatin1Char('/'));
|
||||
if (slash >= 0) {
|
||||
item.label = rel.mid(slash + 1);
|
||||
item.group = rel.left(slash);
|
||||
} else {
|
||||
item.label = rel;
|
||||
}
|
||||
items.push_back(item);
|
||||
}
|
||||
m_filePalette->setItems(items);
|
||||
|
||||
if (QWidget *w = m_mainWindow->window()) {
|
||||
const QSize sz = m_filePalette->size();
|
||||
const QPoint topLeft = w->mapToGlobal(QPoint((w->width() - sz.width()) / 2,
|
||||
w->height() / 6));
|
||||
m_filePalette->move(topLeft);
|
||||
}
|
||||
|
||||
m_filePalette->activate();
|
||||
}
|
||||
|
||||
void OllieView::openProjectFile(const QString &relativePath)
|
||||
{
|
||||
if (relativePath.isEmpty() || m_fileSwitcherRoot.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QString absolute = QDir(m_fileSwitcherRoot).absoluteFilePath(relativePath);
|
||||
const QUrl url = QUrl::fromLocalFile(absolute);
|
||||
if (KTextEditor::View *view = m_mainWindow->openUrl(url)) {
|
||||
m_mainWindow->activateView(view->document());
|
||||
}
|
||||
}
|
||||
|
||||
QAction *OllieView::findActionByName(const QString &nameOrText) const
|
||||
{
|
||||
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ public:
|
|||
private Q_SLOTS:
|
||||
void showCommandPalette();
|
||||
void runAction(const QString &actionId);
|
||||
void showFileSwitcher();
|
||||
void openProjectFile(const QString &relativePath);
|
||||
|
||||
private:
|
||||
// One harvested, runnable action with the provenance needed to build a
|
||||
|
|
@ -105,6 +107,8 @@ private:
|
|||
|
||||
KTextEditor::MainWindow *m_mainWindow;
|
||||
PaletteWidget *m_palette = nullptr;
|
||||
PaletteWidget *m_filePalette = nullptr; // project "Go to File" switcher
|
||||
QString m_fileSwitcherRoot; // project root for the open switcher
|
||||
RadialMenu *m_radial = nullptr;
|
||||
QList<RadialDef> m_radialDefs;
|
||||
QHash<int, int> m_buttonToRadial; // Qt::MouseButton (int) -> def index
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
add_library(project_lib STATIC
|
||||
projectindex.cpp
|
||||
projectindex.h
|
||||
)
|
||||
target_link_libraries(project_lib PUBLIC
|
||||
Qt6::Core
|
||||
)
|
||||
target_include_directories(project_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
if(Qt6Test_FOUND)
|
||||
add_executable(test_projectindex test_projectindex.cpp)
|
||||
target_link_libraries(test_projectindex PRIVATE project_lib Qt6::Core Qt6::Test)
|
||||
add_test(NAME projectindex COMMAND test_projectindex)
|
||||
endif()
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include "projectindex.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QProcess>
|
||||
#include <QSet>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
QStringList ProjectIndex::rootMarkers()
|
||||
{
|
||||
return {
|
||||
QStringLiteral(".git"),
|
||||
QStringLiteral(".hg"),
|
||||
QStringLiteral(".svn"),
|
||||
};
|
||||
}
|
||||
|
||||
QStringList ProjectIndex::ignoredDirs()
|
||||
{
|
||||
return {
|
||||
QStringLiteral(".git"),
|
||||
QStringLiteral(".hg"),
|
||||
QStringLiteral(".svn"),
|
||||
QStringLiteral("node_modules"),
|
||||
QStringLiteral("build"),
|
||||
QStringLiteral(".build"),
|
||||
QStringLiteral("target"),
|
||||
QStringLiteral("dist"),
|
||||
QStringLiteral(".cache"),
|
||||
QStringLiteral("__pycache__"),
|
||||
QStringLiteral(".venv"),
|
||||
QStringLiteral("venv"),
|
||||
};
|
||||
}
|
||||
|
||||
QString ProjectIndex::findRoot(const QString &startDir)
|
||||
{
|
||||
if (startDir.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
QDir dir(startDir);
|
||||
// Accept a file path too: step to its containing directory.
|
||||
if (QFileInfo(startDir).isFile()) {
|
||||
dir = QFileInfo(startDir).dir();
|
||||
}
|
||||
if (!dir.exists()) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QStringList markers = rootMarkers();
|
||||
QDir probe = dir;
|
||||
while (true) {
|
||||
for (const QString &marker : markers) {
|
||||
if (probe.exists(marker)) {
|
||||
return probe.absolutePath();
|
||||
}
|
||||
}
|
||||
if (!probe.cdUp()) {
|
||||
break; // reached the filesystem root with no marker
|
||||
}
|
||||
}
|
||||
return dir.absolutePath();
|
||||
}
|
||||
|
||||
bool ProjectIndex::isGitRoot(const QString &dir)
|
||||
{
|
||||
if (dir.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return QFileInfo::exists(QDir(dir).filePath(QStringLiteral(".git")));
|
||||
}
|
||||
|
||||
QStringList ProjectIndex::gitListFiles(const QString &root)
|
||||
{
|
||||
QProcess git;
|
||||
git.setWorkingDirectory(root);
|
||||
// Tracked files, plus untracked files that are not ignored. --exclude-standard
|
||||
// applies .gitignore / .git/info/exclude / global excludes to the untracked
|
||||
// set. -z would need manual splitting; newline output is sufficient here
|
||||
// (paths with newlines are vanishingly rare and git quotes them otherwise).
|
||||
git.start(QStringLiteral("git"),
|
||||
{QStringLiteral("ls-files"),
|
||||
QStringLiteral("--cached"),
|
||||
QStringLiteral("--others"),
|
||||
QStringLiteral("--exclude-standard")});
|
||||
if (!git.waitForStarted(3000)) {
|
||||
return QStringList();
|
||||
}
|
||||
if (!git.waitForFinished(10000)) {
|
||||
git.kill();
|
||||
git.waitForFinished(1000);
|
||||
return QStringList();
|
||||
}
|
||||
if (git.exitStatus() != QProcess::NormalExit || git.exitCode() != 0) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
const QString out = QString::fromUtf8(git.readAllStandardOutput());
|
||||
QStringList files = out.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
|
||||
// git ls-files de-dups tracked vs untracked already, but be defensive.
|
||||
files.removeDuplicates();
|
||||
files.sort();
|
||||
return files;
|
||||
}
|
||||
|
||||
QStringList ProjectIndex::walkDirectory(const QString &root,
|
||||
const QStringList &ignoreDirs,
|
||||
int maxFiles)
|
||||
{
|
||||
QStringList files;
|
||||
if (root.isEmpty()) {
|
||||
return files;
|
||||
}
|
||||
QDir rootDir(root);
|
||||
if (!rootDir.exists()) {
|
||||
return files;
|
||||
}
|
||||
const QString absRoot = rootDir.absolutePath();
|
||||
const QSet<QString> ignore(ignoreDirs.cbegin(), ignoreDirs.cend());
|
||||
|
||||
// Explicit stack walk so ignored directories are never descended into
|
||||
// (QDirIterator has no per-directory prune hook).
|
||||
QStringList stack;
|
||||
stack.push_back(absRoot);
|
||||
while (!stack.isEmpty()) {
|
||||
const QString current = stack.takeLast();
|
||||
QDir dir(current);
|
||||
const QFileInfoList entries =
|
||||
dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot | QDir::Hidden);
|
||||
for (const QFileInfo &info : entries) {
|
||||
if (info.isSymLink()) {
|
||||
continue; // avoid cycles and escaping the tree
|
||||
}
|
||||
if (info.isDir()) {
|
||||
if (ignore.contains(info.fileName())) {
|
||||
continue;
|
||||
}
|
||||
stack.push_back(info.absoluteFilePath());
|
||||
} else if (info.isFile()) {
|
||||
const QString rel = QDir(absRoot).relativeFilePath(info.absoluteFilePath());
|
||||
files.push_back(rel);
|
||||
if (maxFiles > 0 && files.size() >= maxFiles) {
|
||||
files.sort();
|
||||
return files;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
return files;
|
||||
}
|
||||
|
||||
QStringList ProjectIndex::listFiles(const QString &root)
|
||||
{
|
||||
if (root.isEmpty() || !QDir(root).exists()) {
|
||||
return QStringList();
|
||||
}
|
||||
if (isGitRoot(root)) {
|
||||
const QStringList git = gitListFiles(root);
|
||||
if (!git.isEmpty()) {
|
||||
return git;
|
||||
}
|
||||
// Fall through to the walk if git produced nothing (e.g. git missing).
|
||||
}
|
||||
return walkDirectory(root, ignoredDirs());
|
||||
}
|
||||
|
||||
} // namespace katecustom
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*
|
||||
* ProjectIndex — "open folder == open project".
|
||||
*
|
||||
* A project is just a directory. The root is discovered by walking up from a
|
||||
* starting path (typically the active document's directory) until a project
|
||||
* marker is found (.git, .hg, .svn). With no marker the starting directory is
|
||||
* the root, so an ad-hoc folder is still a usable project.
|
||||
*
|
||||
* File enumeration prefers `git ls-files` inside a git work tree (fast, honours
|
||||
* .gitignore, includes untracked-but-not-ignored files). Outside git it falls
|
||||
* back to a bounded recursive directory walk that skips well-known noise dirs.
|
||||
*
|
||||
* The matching/ranking of the returned list is left to FuzzyRanker /
|
||||
* PaletteWidget; this class only produces the candidate set.
|
||||
*/
|
||||
#ifndef KATECUSTOM_PROJECTINDEX_H
|
||||
#define KATECUSTOM_PROJECTINDEX_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
class ProjectIndex
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* Directory names that mark a project root when encountered while walking
|
||||
* up from a start path. The first match wins.
|
||||
*/
|
||||
static QStringList rootMarkers();
|
||||
|
||||
/*!
|
||||
* Directory names skipped by the non-git recursive walk. Keeps the index
|
||||
* free of build output and vendored dependencies.
|
||||
*/
|
||||
static QStringList ignoredDirs();
|
||||
|
||||
/*!
|
||||
* Walk up from \a startDir (a directory, or a file's directory) looking for
|
||||
* a \c rootMarkers() entry. Returns the first ancestor that contains one, or
|
||||
* \a startDir itself when none is found. Returns an empty string if
|
||||
* \a startDir is empty or does not exist.
|
||||
*/
|
||||
static QString findRoot(const QString &startDir);
|
||||
|
||||
/*!
|
||||
* List project files as paths relative to \a root, using git when \a root is
|
||||
* a git work tree, otherwise a recursive walk. Returns an empty list when
|
||||
* \a root is empty or missing. Results are sorted.
|
||||
*/
|
||||
static QStringList listFiles(const QString &root);
|
||||
|
||||
/*!
|
||||
* Recursive directory walk returning file paths relative to \a root, sorted.
|
||||
* Directories whose name is in \a ignoreDirs are not descended into. Pure;
|
||||
* no external process. \a maxFiles caps the result to keep huge trees sane
|
||||
* (0 = no cap).
|
||||
*/
|
||||
static QStringList walkDirectory(const QString &root,
|
||||
const QStringList &ignoreDirs,
|
||||
int maxFiles = 50000);
|
||||
|
||||
/*!
|
||||
* True if \a dir is the top of a git work tree, i.e. a `.git` entry exists
|
||||
* directly in \a dir.
|
||||
*/
|
||||
static bool isGitRoot(const QString &dir);
|
||||
|
||||
private:
|
||||
/*! Run `git -C root ls-files` (tracked + untracked-not-ignored), sorted. */
|
||||
static QStringList gitListFiles(const QString &root);
|
||||
};
|
||||
|
||||
} // namespace katecustom
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*
|
||||
* Unit tests for ProjectIndex. Exercises the pure logic (root discovery and the
|
||||
* recursive directory walk) against a temporary tree. The git path is covered
|
||||
* only indirectly: listFiles() falls back to the walk when the tree is not a
|
||||
* git work tree.
|
||||
*/
|
||||
#include "projectindex.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
|
||||
using namespace katecustom;
|
||||
|
||||
namespace
|
||||
{
|
||||
void touch(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
QVERIFY2(f.open(QIODevice::WriteOnly), qPrintable(path));
|
||||
f.write("x");
|
||||
f.close();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class TestProjectIndex : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void findRootWalksUpToMarker()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
QDir base(tmp.path());
|
||||
QVERIFY(base.mkpath(QStringLiteral(".git")));
|
||||
QVERIFY(base.mkpath(QStringLiteral("src/sub")));
|
||||
|
||||
const QString start = base.filePath(QStringLiteral("src/sub"));
|
||||
const QString root = ProjectIndex::findRoot(start);
|
||||
QCOMPARE(QDir(root).canonicalPath(), QDir(tmp.path()).canonicalPath());
|
||||
}
|
||||
|
||||
void findRootAcceptsFilePath()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
QDir base(tmp.path());
|
||||
QVERIFY(base.mkpath(QStringLiteral(".git")));
|
||||
QVERIFY(base.mkpath(QStringLiteral("src")));
|
||||
const QString file = base.filePath(QStringLiteral("src/main.cpp"));
|
||||
touch(file);
|
||||
|
||||
const QString root = ProjectIndex::findRoot(file);
|
||||
QCOMPARE(QDir(root).canonicalPath(), QDir(tmp.path()).canonicalPath());
|
||||
}
|
||||
|
||||
void findRootFallsBackToStartDir()
|
||||
{
|
||||
QTemporaryDir tmp; // no marker anywhere
|
||||
QVERIFY(tmp.isValid());
|
||||
QDir base(tmp.path());
|
||||
QVERIFY(base.mkpath(QStringLiteral("a/b")));
|
||||
const QString start = base.filePath(QStringLiteral("a/b"));
|
||||
|
||||
const QString root = ProjectIndex::findRoot(start);
|
||||
QCOMPARE(QDir(root).canonicalPath(), QDir(start).canonicalPath());
|
||||
}
|
||||
|
||||
void findRootEmptyOrMissing()
|
||||
{
|
||||
QCOMPARE(ProjectIndex::findRoot(QString()), QString());
|
||||
QCOMPARE(ProjectIndex::findRoot(QStringLiteral("/no/such/path/xyz")), QString());
|
||||
}
|
||||
|
||||
void walkListsFilesRelativeAndSorted()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
QDir base(tmp.path());
|
||||
QVERIFY(base.mkpath(QStringLiteral("src")));
|
||||
touch(base.filePath(QStringLiteral("README.md")));
|
||||
touch(base.filePath(QStringLiteral("src/a.cpp")));
|
||||
touch(base.filePath(QStringLiteral("src/b.cpp")));
|
||||
|
||||
const QStringList files = ProjectIndex::walkDirectory(tmp.path(), {});
|
||||
QStringList expected{QStringLiteral("README.md"),
|
||||
QStringLiteral("src/a.cpp"),
|
||||
QStringLiteral("src/b.cpp")};
|
||||
QCOMPARE(files, expected);
|
||||
}
|
||||
|
||||
void walkSkipsIgnoredDirs()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
QDir base(tmp.path());
|
||||
QVERIFY(base.mkpath(QStringLiteral("node_modules/pkg")));
|
||||
QVERIFY(base.mkpath(QStringLiteral(".git")));
|
||||
QVERIFY(base.mkpath(QStringLiteral("src")));
|
||||
touch(base.filePath(QStringLiteral("src/main.cpp")));
|
||||
touch(base.filePath(QStringLiteral("node_modules/pkg/index.js")));
|
||||
touch(base.filePath(QStringLiteral(".git/config")));
|
||||
|
||||
const QStringList files =
|
||||
ProjectIndex::walkDirectory(tmp.path(), ProjectIndex::ignoredDirs());
|
||||
QCOMPARE(files, QStringList{QStringLiteral("src/main.cpp")});
|
||||
}
|
||||
|
||||
void walkRespectsMaxFiles()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
QDir base(tmp.path());
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
touch(base.filePath(QStringLiteral("f%1.txt").arg(i)));
|
||||
}
|
||||
const QStringList files = ProjectIndex::walkDirectory(tmp.path(), {}, 3);
|
||||
QCOMPARE(files.size(), 3);
|
||||
}
|
||||
|
||||
void isGitRootDetectsMarker()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
QVERIFY(!ProjectIndex::isGitRoot(tmp.path()));
|
||||
QVERIFY(QDir(tmp.path()).mkpath(QStringLiteral(".git")));
|
||||
QVERIFY(ProjectIndex::isGitRoot(tmp.path()));
|
||||
}
|
||||
|
||||
void listFilesFallsBackToWalkWhenNoGit()
|
||||
{
|
||||
QTemporaryDir tmp; // not a git root
|
||||
QVERIFY(tmp.isValid());
|
||||
QDir base(tmp.path());
|
||||
touch(base.filePath(QStringLiteral("one.txt")));
|
||||
touch(base.filePath(QStringLiteral("two.txt")));
|
||||
const QStringList files = ProjectIndex::listFiles(tmp.path());
|
||||
QStringList expected{QStringLiteral("one.txt"), QStringLiteral("two.txt")};
|
||||
QCOMPARE(files, expected);
|
||||
}
|
||||
|
||||
void listFilesEmptyRoot()
|
||||
{
|
||||
QCOMPARE(ProjectIndex::listFiles(QString()), QStringList());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestProjectIndex)
|
||||
#include "test_projectindex.moc"
|
||||
Loading…
Reference in New Issue