summaryrefslogtreecommitdiff
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
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.
-rw-r--r--src/completion.cc121
-rw-r--r--src/completion.hh15
-rw-r--r--src/file.cc148
-rw-r--r--src/file.hh24
-rw-r--r--src/main.cc8
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;
}