summaryrefslogtreecommitdiff
path: root/src/completion.cc
diff options
context:
space:
mode:
authorMaxime Coste <mawww@kakoune.org>2025-02-19 22:03:56 +1100
committerMaxime Coste <mawww@kakoune.org>2025-02-19 23:02:03 +1100
commitf910d6cb657ff39df5c536929e4146f88acda93f (patch)
tree451adb66d06cbb2353f0536febb01c048e6f4f24 /src/completion.cc
parenteb7d34333b0698e8e1a8af4f2be908ab08cb2089 (diff)
Move command/filename completion logic to completion.cc
Refactor list_files to use a callback instead of returning a vector, file.cc/hh should not know about completion logic.
Diffstat (limited to 'src/completion.cc')
-rw-r--r--src/completion.cc121
1 files changed, 121 insertions, 0 deletions
diff --git a/src/completion.cc b/src/completion.cc
index c44319d8..d1e10f08 100644
--- a/src/completion.cc
+++ b/src/completion.cc
@@ -8,6 +8,127 @@
namespace Kakoune
{
+static CandidateList candidates(ConstArrayView<RankedMatch> matches, StringView dirname)
+{
+ CandidateList res;
+ res.reserve(matches.size());
+ for (auto& match : matches)
+ res.push_back(dirname + match.candidate());
+ return res;
+}
+
+CandidateList complete_filename(StringView prefix, const Regex& ignored_regex,
+ ByteCount cursor_pos, FilenameFlags flags)
+{
+ prefix = prefix.substr(0, cursor_pos);
+ auto [dirname, fileprefix] = split_path(prefix);
+ auto parsed_dirname = parse_filename(dirname);
+
+ Optional<ThreadedRegexVM<const char*, RegexMode::Forward | RegexMode::AnyMatch | RegexMode::NoSaves>> vm;
+ if (not ignored_regex.empty())
+ {
+ vm.emplace(*ignored_regex.impl());
+ if (vm->exec(fileprefix.begin(), fileprefix.end(), fileprefix.begin(), fileprefix.end(), RegexExecFlags::None))
+ vm.reset();
+ }
+
+ const bool only_dirs = (flags & FilenameFlags::OnlyDirectories);
+
+ Vector<String> files;
+ list_files(parsed_dirname, [&](StringView filename, const struct stat& st) {
+ if ((not vm or not vm->exec(filename.begin(), filename.end(),
+ filename.begin(), filename.end(),
+ RegexExecFlags::None)) and
+ (not only_dirs or S_ISDIR(st.st_mode)))
+ files.push_back(filename.str());
+ });
+ Vector<RankedMatch> matches;
+ for (auto& file : files)
+ {
+ if (RankedMatch match{file, fileprefix})
+ matches.push_back(match);
+ }
+ // Hack: when completing directories, also echo back the query if it
+ // is a valid directory. This enables menu completion to select the
+ // directory instead of a child.
+ if (only_dirs and not dirname.empty() and dirname.back() == '/' and fileprefix.empty()
+ and /* exists on disk */ not files.empty())
+ {
+ matches.push_back(RankedMatch{fileprefix, fileprefix});
+ }
+ std::sort(matches.begin(), matches.end());
+ const bool expand = (flags & FilenameFlags::Expand);
+ return candidates(matches, expand ? parsed_dirname : dirname);
+}
+
+CandidateList complete_command(StringView prefix, ByteCount cursor_pos)
+{
+ String real_prefix = parse_filename(prefix.substr(0, cursor_pos));
+ auto [dirname, fileprefix] = split_path(real_prefix);
+
+ if (not dirname.empty())
+ {
+ Vector<String> files;
+ list_files(dirname, [&](StringView filename, const struct stat& st) {
+ bool executable = (st.st_mode & S_IXUSR)
+ | (st.st_mode & S_IXGRP)
+ | (st.st_mode & S_IXOTH);
+ if (S_ISDIR(st.st_mode) or (S_ISREG(st.st_mode) and executable))
+ files.push_back(filename.str());
+ });
+ Vector<RankedMatch> matches;
+ for (auto& file : files)
+ {
+ if (RankedMatch match{file, fileprefix})
+ matches.push_back(match);
+ }
+ std::sort(matches.begin(), matches.end());
+ return candidates(matches, dirname);
+ }
+
+ using TimeSpec = decltype(stat::st_mtim);
+
+ struct CommandCache
+ {
+ TimeSpec mtim = {};
+ Vector<String, MemoryDomain::Completion> commands;
+ };
+ static HashMap<String, CommandCache, MemoryDomain::Completion> command_cache;
+
+ Vector<RankedMatch> matches;
+ for (auto dir : StringView{getenv("PATH")} | split<StringView>(':'))
+ {
+ auto dirname = ((not dir.empty() and dir.back() == '/') ? dir.substr(0, dir.length()-1) : dir).str();
+
+ struct stat st;
+ if (stat(dirname.c_str(), &st))
+ continue;
+
+ auto& cache = command_cache[dirname];
+ if (memcmp(&cache.mtim, &st.st_mtim, sizeof(TimeSpec)) != 0)
+ {
+ cache.commands.clear();
+ list_files(dirname, [&](StringView filename, const struct stat& st) {
+ bool executable = (st.st_mode & S_IXUSR)
+ | (st.st_mode & S_IXGRP)
+ | (st.st_mode & S_IXOTH);
+ if (S_ISREG(st.st_mode) and executable)
+ cache.commands.push_back(filename.str());
+ });
+ memcpy(&cache.mtim, &st.st_mtim, sizeof(TimeSpec));
+ }
+ for (auto& cmd : cache.commands)
+ {
+ if (RankedMatch match{cmd, fileprefix})
+ matches.push_back(match);
+ }
+ }
+ std::sort(matches.begin(), matches.end());
+ auto it = std::unique(matches.begin(), matches.end());
+ matches.erase(it, matches.end());
+ return candidates(matches, "");
+}
+
Completions shell_complete(const Context& context, StringView prefix, ByteCount cursor_pos)
{
ByteCount word_start = 0;