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 | |
| 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.
| -rw-r--r-- | src/completion.cc | 121 | ||||
| -rw-r--r-- | src/completion.hh | 15 | ||||
| -rw-r--r-- | src/file.cc | 148 | ||||
| -rw-r--r-- | src/file.hh | 24 | ||||
| -rw-r--r-- | src/main.cc | 8 |
5 files changed, 147 insertions, 169 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; diff --git a/src/completion.hh b/src/completion.hh index cef703a2..0f13e9bf 100644 --- a/src/completion.hh +++ b/src/completion.hh @@ -12,6 +12,7 @@ namespace Kakoune { class Context; +class Regex; using CandidateList = Vector<String, MemoryDomain::Completion>; @@ -47,6 +48,20 @@ inline Completions complete_nothing(const Context&, StringView, ByteCount cursor return {cursor_pos, cursor_pos}; } +enum class FilenameFlags +{ + None = 0, + OnlyDirectories = 1 << 0, + Expand = 1 << 1 +}; +constexpr bool with_bit_ops(Meta::Type<FilenameFlags>) { return true; } + +CandidateList complete_filename(StringView prefix, const Regex& ignore_regex, + ByteCount cursor_pos = -1, + FilenameFlags flags = FilenameFlags::None); + +CandidateList complete_command(StringView prefix, ByteCount cursor_pos = -1); + Completions shell_complete(const Context& context, StringView, ByteCount cursor_pos); inline Completions offset_pos(Completions completion, ByteCount offset) diff --git a/src/file.cc b/src/file.cc index b434e9dc..67781044 100644 --- a/src/file.cc +++ b/src/file.cc @@ -1,15 +1,11 @@ #include "file.hh" #include "assert.hh" -#include "flags.hh" #include "event_manager.hh" -#include "ranked_match.hh" -#include "regex.hh" #include "string.hh" #include "string_utils.hh" #include "format.hh" #include "ranges.hh" -#include "hash_map.hh" #include <limits> #include <cerrno> @@ -370,18 +366,16 @@ void make_directory(StringView dir, mode_t mode) } } -template<MemoryDomain domain = MemoryDomain::Undefined, typename Filter> -Vector<String, domain> list_files(StringView dirname, Filter filter) +void list_files(StringView dirname, FunctionRef<void (StringView, const struct stat&)> callback) { char buffer[PATH_MAX+1]; format_to(buffer, "{}", dirname); DIR* dir = opendir(dirname.empty() ? "./" : buffer); if (not dir) - return {}; + return; auto close_dir = on_scope_end([dir]{ closedir(dir); }); - Vector<String, domain> result; while (dirent* entry = readdir(dir)) { StringView filename = entry->d_name; @@ -391,147 +385,13 @@ Vector<String, domain> list_files(StringView dirname, Filter filter) struct stat st; auto fmt_str = (dirname.empty() or dirname.back() == '/') ? "{}{}" : "{}/{}"; format_to(buffer, fmt_str, dirname, filename); - if (stat(buffer, &st) != 0 or not filter(*entry, st)) + if (stat(buffer, &st) != 0) continue; if (S_ISDIR(st.st_mode)) filename = format_to(buffer, "{}/", filename); - result.push_back(filename.str()); - } - return result; -} - -template<MemoryDomain domain = MemoryDomain::Undefined> -Vector<String, domain> list_files(StringView directory) -{ - return list_files(directory, [](const dirent& entry, const struct stat&) { - return StringView{entry.d_name}.substr(0_byte, 1_byte) != "."; - }); -} - -Vector<String> list_files(StringView directory) -{ - return list_files<>(directory); -} - -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); - - auto filter = [&vm, only_dirs](const dirent& entry, struct stat& st) - { - StringView name{entry.d_name}; - return (not vm or not vm->exec(name.begin(), name.end(), name.begin(), name.end(), RegexExecFlags::None)) and - (not only_dirs or S_ISDIR(st.st_mode)); - }; - auto files = list_files(parsed_dirname, filter); - 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()) - { - auto filter = [](const dirent& entry, const struct stat& st) - { - bool executable = (st.st_mode & S_IXUSR) - | (st.st_mode & S_IXGRP) - | (st.st_mode & S_IXOTH); - return S_ISDIR(st.st_mode) or (S_ISREG(st.st_mode) and executable); - }; - auto files = list_files(dirname, filter); - 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::Commands> 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) - { - auto filter = [](const dirent& entry, const struct stat& st) { - bool executable = (st.st_mode & S_IXUSR) - | (st.st_mode & S_IXGRP) - | (st.st_mode & S_IXOTH); - return S_ISREG(st.st_mode) and executable; - }; - - cache.commands = list_files<MemoryDomain::Completion>(dirname, filter); - memcpy(&cache.mtim, &st.st_mtim, sizeof(TimeSpec)); - } - for (auto& cmd : cache.commands) - { - if (RankedMatch match{cmd, fileprefix}) - matches.push_back(match); - } + callback(filename, st); } - std::sort(matches.begin(), matches.end()); - auto it = std::unique(matches.begin(), matches.end()); - matches.erase(it, matches.end()); - return candidates(matches, ""); } timespec get_fs_timestamp(StringView filename) diff --git a/src/file.hh b/src/file.hh index 3d0d8603..0630a2f7 100644 --- a/src/file.hh +++ b/src/file.hh @@ -8,7 +8,7 @@ #include "meta.hh" #include "string.hh" #include "units.hh" -#include "vector.hh" +#include "utils.hh" #include <sys/types.h> #include <sys/stat.h> @@ -17,18 +17,12 @@ namespace Kakoune { -class String; -class Regex; - struct file_access_error : runtime_error { file_access_error(StringView filename, StringView error_desc); file_access_error(int fd, StringView error_desc); }; - -using CandidateList = Vector<String, MemoryDomain::Completion>; - // parse ~/ and %/ in filename and returns the translated filename String parse_filename(StringView filename, StringView buf_dir = {}); @@ -82,7 +76,7 @@ String find_file(StringView filename, StringView buf_dir, ConstArrayView<String> bool file_exists(StringView filename); bool regular_file_exists(StringView filename); -Vector<String> list_files(StringView directory); +void list_files(StringView directory, FunctionRef<void (StringView, const struct stat&)> callback); void make_directory(StringView dir, mode_t mode); @@ -103,20 +97,6 @@ constexpr bool operator==(const timespec& lhs, const timespec& rhs) return lhs.tv_sec == rhs.tv_sec and lhs.tv_nsec == rhs.tv_nsec; } -enum class FilenameFlags -{ - None = 0, - OnlyDirectories = 1 << 0, - Expand = 1 << 1 -}; -constexpr bool with_bit_ops(Meta::Type<FilenameFlags>) { return true; } - -CandidateList complete_filename(StringView prefix, const Regex& ignore_regex, - ByteCount cursor_pos = -1, - FilenameFlags flags = FilenameFlags::None); - -CandidateList complete_command(StringView prefix, ByteCount cursor_pos = -1); - template<bool atomic, int buffer_size = 4096> struct BufferedWriter { diff --git a/src/main.cc b/src/main.cc index 0d70bd6a..e95725ec 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1154,14 +1154,16 @@ int main(int argc, char* argv[]) const bool clear_sessions = (bool)parser.get_switch("clear"); if (list_sessions or clear_sessions) { - for (auto& session : list_files(session_directory())) - { + list_files(session_directory(), [&](StringView session, auto&) { + if (session.substr(0_byte, 1_byte) == ".") + return; + const bool valid = check_session(session); if (list_sessions) write_stdout(format("{}{}\n", session, valid ? "" : " (dead)")); if (not valid and clear_sessions) unlink(session_path(session).c_str()); - } + }); return 0; } |
