diff options
| author | Maxime Coste <mawww@kakoune.org> | 2025-02-19 22:03:56 +1100 |
|---|---|---|
| committer | Maxime Coste <mawww@kakoune.org> | 2025-02-19 23:02:03 +1100 |
| commit | f910d6cb657ff39df5c536929e4146f88acda93f (patch) | |
| tree | 451adb66d06cbb2353f0536febb01c048e6f4f24 /src/completion.cc | |
| parent | eb7d34333b0698e8e1a8af4f2be908ab08cb2089 (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.cc | 121 |
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; |
