#include "RepoFileRows.h" #include #include #include #include "FuzzyMatch.h" namespace { // The directory that holds `relative`, relative to the root. An empty string // means the file sits at the root. [[nodiscard]] QString relativeDirectory(const QString& relative) { const qsizetype slash = relative.lastIndexOf(QLatin1Char('/')); return slash < 0 ? QString() : relative.left(slash); } // The file name of a workspace-root-relative path. [[nodiscard]] QString relativeName(const QString& relative) { const qsizetype slash = relative.lastIndexOf(QLatin1Char('/')); return slash < 0 ? relative : relative.mid(slash + 1); } // One candidate with the keys the order reads, so each key costs one call. struct Candidate { QString relative; QString absolute; int frecency = 0; int score = 0; bool pathMatch = false; bool nameMatch = false; }; // The order of the header, best first. Every key is a total order, so the // result is the same on every run. [[nodiscard]] bool better(const Candidate& left, const Candidate& right) { if (left.pathMatch != right.pathMatch) { return left.pathMatch; } if (left.nameMatch != right.nameMatch) { return left.nameMatch; } if (left.frecency != right.frecency) { return left.frecency > right.frecency; } if (left.score != right.score) { return left.score > right.score; } if (left.relative.size() != right.relative.size()) { return left.relative.size() < right.relative.size(); } return left.relative < right.relative; } // One file row: the name, the holding directory, the kind, and the absolute // path. [[nodiscard]] QVariantMap fileRow(const QString& relative, const QString& absolute) { QVariantMap row; row.insert(QStringLiteral("title"), relativeName(relative)); row.insert(QStringLiteral("subtitle"), relativeDirectory(relative)); row.insert(QStringLiteral("kind"), QStringLiteral("file")); row.insert(QStringLiteral("path"), absolute); return row; } // The terminal row. Its subtitle is the directory the terminal spawns in. [[nodiscard]] QVariantMap terminalRow(const QString& root) { QVariantMap row; row.insert(QStringLiteral("title"), QStringLiteral("Terminal")); row.insert(QStringLiteral("subtitle"), root); row.insert(QStringLiteral("kind"), QStringLiteral("terminal")); row.insert(QStringLiteral("path"), QString()); return row; } } // namespace void splitRepoQuery(const QString& query, QString* prefix, QString* partial) { const qsizetype slash = query.lastIndexOf(QLatin1Char('/')); if (slash < 0) { *prefix = QString(); *partial = query; return; } *prefix = query.left(slash + 1); *partial = query.mid(slash + 1); } RepoRows rankRepoRows(const QStringList& files, const QString& root, const QString& query, bool includeTerminalRow, const std::function& frecency) { QString prefix; QString partial; splitRepoQuery(query, &prefix, &partial); const bool hasPartial = !partial.isEmpty(); QList candidates; for (const QString& relative : files) { // The prefix keeps a file only when the path starts with it exactly, so // `a/foo.c` never reaches `ab/foo.c`. if (!relative.startsWith(prefix, Qt::CaseInsensitive)) { continue; } const QString rest = relative.mid(prefix.size()); // The partial matches the path after the prefix and the file name, and the // better of the two stands. The name alone carries no penalty for the // directories above it, so a file deep in the tree still ranks on its own // name. The directory walk ranks its rows the same way. int score = 0; if (hasPartial) { const int restScore = fuzzyScore(rest, partial); // A non-empty partial must fuzzy-match the text after the prefix. if (restScore < 0) { continue; } score = std::max(restScore, fuzzyScore(relativeName(relative), partial)); } const QString absolute = root.isEmpty() ? relative : root + QLatin1Char('/') + relative; candidates.append(Candidate{ .relative = relative, .absolute = absolute, .frecency = frecency(absolute), .score = score, .pathMatch = relative.compare(query, Qt::CaseInsensitive) == 0, .nameMatch = hasPartial && relativeName(relative).compare( partial, Qt::CaseInsensitive) == 0, }); } std::ranges::sort(candidates, better); RepoRows out; out.capped = candidates.size() > kMaxRepoRows; const qsizetype shown = std::min(candidates.size(), kMaxRepoRows); out.rows.reserve(shown + (includeTerminalRow ? 1 : 0)); for (const Candidate& candidate : candidates | std::views::take(shown)) { out.rows.append(fileRow(candidate.relative, candidate.absolute)); } // The terminal row is last and never first. On an empty query the first row // is what Enter takes, and that row must be the top file. if (includeTerminalRow && (query.isEmpty() || fuzzyScore(QStringLiteral("Terminal"), query) >= 0)) { out.rows.append(terminalRow(root)); } // An empty listing names what was searched, so it is never silent. A listing // cut at the cap says so as well, because a short list otherwise reads as the // whole answer. if (out.rows.isEmpty()) { out.note = query.isEmpty() ? QStringLiteral("the repository lists no files") : QStringLiteral("nothing here matches \"%1\"").arg(query); } else if (out.capped) { out.note = QStringLiteral("showing the first %1 matches").arg(kMaxRepoRows); } return out; }