#include "CLIForge/Levenshtein.hpp" namespace cliforge::detail { std::size_t levenshtein(const std::string& a, const std::string& b) { const std::size_t n = a.size(), m = b.size(); std::vector prev(m + 1), cur(m + 1); for (std::size_t j = 0; j <= m; ++j) prev[j] = j; for (std::size_t i = 1; i <= n; ++i) { cur[0] = i; for (std::size_t j = 1; j <= m; ++j) { std::size_t cost = (a[i - 1] == b[j - 1]) ? 0 : 1; cur[j] = std::min({prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost}); } std::swap(prev, cur); } return prev[m]; } // Returns up to `limit` candidates from `pool` that are close to `query`, // ordered by distance. Anything farther than a generous relative // threshold is dropped so we never suggest something wildly unrelated. std::vector closestMatches(const std::string& query, const std::vector& pool, std::size_t limit) { std::vector> scored; scored.reserve(pool.size()); for (const auto& candidate : pool) { std::size_t d = levenshtein(query, candidate); // A prefix relationship (e.g. "--arch" for "--architectures") is a // very strong signal -- treat it as near-identical even though its // raw edit distance is large, so common abbreviations still surface. if (query.size() >= 2 && candidate.size() > query.size() && candidate.compare(0, query.size(), query) == 0) { d = std::min(d, 1); } scored.emplace_back(d, candidate); } std::sort(scored.begin(), scored.end(), [](auto& a, auto& b) { return a.first < b.first; }); std::size_t threshold = std::max(3, query.size() / 2); std::vector out; for (auto& [dist, name] : scored) { if (out.size() >= limit) break; if (dist <= threshold) out.push_back(name); } return out; } }