diff --git a/doc/pages/commands.asciidoc b/doc/pages/commands.asciidoc index 5b158ef838..66d4857625 100644 --- a/doc/pages/commands.asciidoc +++ b/doc/pages/commands.asciidoc @@ -482,6 +482,7 @@ New commands can be defined using the *define-command* command: define the documentation string for the command *-menu*::: + *-priority*::: *-file-completion*::: *-client-completion*::: *-buffer-completion*::: @@ -517,6 +518,9 @@ Command completion can be configured with the *complete-command* command: permitted parameters. Kakoune will autoselect the best completion candidate on command validation. + *-priority*::: + see `shell-script-candidates`. + *completion_type* can be: *file*::: @@ -557,6 +561,13 @@ Command completion can be configured with the *complete-command* command: completion session, candidates are cached and then used by kakoune internal fuzzy engine. + If the `-priority` switch is specified, shell script output lines + must match `|`. In this case, any `|` or `\` + characters that occur within the `` field, should be + escaped as `\|` or `\\`. The priority field is a positive integer + to sort completions (lower is better), same as in the `completions` + type in <>. + During the execution of the shell script, the following env vars are available: diff --git a/doc/pages/options.asciidoc b/doc/pages/options.asciidoc index 4f69cbfb7e..20a7362ec8 100644 --- a/doc/pages/options.asciidoc +++ b/doc/pages/options.asciidoc @@ -142,7 +142,7 @@ are exclusively available to built-in options. escaped as `\|` or `\\`. *completions*:: - a list of `||[|]` candidates, except for the first element which follows the `.[+]@` format to define where the completion apply in the buffer. @@ -152,8 +152,11 @@ are exclusively available to built-in options. Options of this type are are meant to be added to the `completers` option to provide insert mode completion. Candidates are shown if the - text typed by the user (between `.` and the cursor) is a - subsequence of ``. + query typed by the user (the text between `.` and the + cursor) is a subsequence of ``. Candidates are sorted by how well + they match the query - by length of matching subsequences and number + of matched word boundaries (higher is better) as well as the optional + `` field, which is a positive integer (lower is better). For each remaining candidate, the completion menu displays ``, followed by ``, which is a Markup string (see diff --git a/gdb/kakoune.py b/gdb/kakoune.py index 11b5714849..94f4db0bfc 100644 --- a/gdb/kakoune.py +++ b/gdb/kakoune.py @@ -206,6 +206,16 @@ def __init__(self, val): def to_string(self): return "regex%s" % (self.val["m_str"]) +class SubsequenceDistance: + """Print a SubsequenceDistance""" + + def __init__(self, val): + self.val = val + + def to_string(self): + reference = "*(%s*)%s" % (self.val.type, self.val.address) + return gdb.parse_and_eval("Kakoune::to_string(%s)" % reference) + def build_pretty_printer(): pp = gdb.printing.RegexpCollectionPrettyPrinter("kakoune") @@ -225,4 +235,5 @@ def build_pretty_printer(): pp.add_printer('ByteCount', '^Kakoune::ByteCount$', ByteCount) pp.add_printer('Color', '^Kakoune::Color$', Color) pp.add_printer('Regex', '^Kakoune::Regex$', Regex) + pp.add_printer('SubsequenceDistance', '^Kakoune::SubsequenceDistance$', SubsequenceDistance) return pp diff --git a/rc/tools/doc.kak b/rc/tools/doc.kak index b4a3b45bec..a90abc32a5 100644 --- a/rc/tools/doc.kak +++ b/rc/tools/doc.kak @@ -134,7 +134,7 @@ define-command -params 1 -hidden doc-render %{ map buffer normal :doc-follow-link } -define-command doc -params 0..2 -menu -docstring %{ +define-command doc -params 0..2 -docstring %{ doc []: open a buffer containing documentation about a given topic An optional keyword argument can be passed to the function, which will be automatically selected in the documentation @@ -165,7 +165,7 @@ define-command doc -params 0..2 -menu -docstring %{ } } -complete-command doc shell-script-candidates %{ +complete-command -menu doc shell-script-candidates %{ case "$kak_token_to_complete" in 0) find -L \ diff --git a/src/array_view.hh b/src/array_view.hh index 982b436f40..00d0f5fbac 100644 --- a/src/array_view.hh +++ b/src/array_view.hh @@ -9,7 +9,7 @@ namespace Kakoune // An ArrayView provides a typed, non owning view of a memory // range with an interface similar to std::vector. -template +template class ArrayView { public: @@ -21,7 +21,7 @@ public: constexpr ArrayView(T& oneval) : m_pointer(&oneval), m_size(1) {} - constexpr ArrayView(T* pointer, size_t size) + constexpr ArrayView(T* pointer, SizeType size) : m_pointer(pointer), m_size(size) {} constexpr ArrayView(T* begin, T* end) @@ -39,10 +39,10 @@ public: : m_pointer(v.begin()), m_size(v.size()) {} constexpr T* pointer() const { return m_pointer; } - constexpr size_t size() const { return m_size; } + constexpr SizeType size() const { return m_size; } [[gnu::always_inline]] - constexpr T& operator[](size_t n) const { return *(m_pointer + n); } + constexpr T& operator[](SizeType n) const { return *(m_pointer + (size_t)n); } constexpr T* begin() const { return m_pointer; } constexpr T* end() const { return m_pointer+m_size; } @@ -56,23 +56,24 @@ public: constexpr bool empty() const { return m_size == 0; } - constexpr ArrayView subrange(size_t first, size_t count = -1) const + constexpr ArrayView subrange(SizeType first, SizeType count = -1) const { - auto min = [](size_t a, size_t b) { return a < b ? a : b; }; + auto min = [](SizeType a, SizeType b) { return a < b ? a : b; }; return ArrayView(m_pointer + min(first, m_size), min(count, m_size - min(first, m_size))); } private: T* m_pointer; - size_t m_size; + SizeType m_size; }; -template -using ConstArrayView = ArrayView; +template +using ConstArrayView = ArrayView; -template -bool operator==(ArrayView lhs, ArrayView rhs) + +template +bool operator==(ArrayView lhs, ArrayView rhs) { if (lhs.size() != rhs.size()) return false; @@ -84,8 +85,8 @@ bool operator==(ArrayView lhs, ArrayView rhs) return true; } -template -bool operator!=(ArrayView lhs, ArrayView rhs) +template +bool operator!=(ArrayView lhs, ArrayView rhs) { return not (lhs == rhs); } diff --git a/src/commands.cc b/src/commands.cc index 6edc308b6b..c95021ff70 100644 --- a/src/commands.cc +++ b/src/commands.cc @@ -165,6 +165,7 @@ static Completions complete_buffer_name(const Context& context, CompletionFlags }; StringView query = prefix.substr(0, cursor_pos); + RankedMatchQuery q{query}; Vector filename_matches; Vector matches; for (const auto& buffer : BufferManager::instance()) @@ -175,13 +176,13 @@ static Completions complete_buffer_name(const Context& context, CompletionFlags StringView bufname = buffer->display_name(); if (buffer->flags() & Buffer::Flags::File) { - if (RankedMatch match{split_path(bufname).second, query}) + if (RankedMatch match{split_path(bufname).second, q}) { filename_matches.emplace_back(match, buffer.get()); continue; } } - if (RankedMatch match{bufname, query}) + if (RankedMatch match{bufname, q}) matches.emplace_back(match, buffer.get()); } std::sort(filename_matches.begin(), filename_matches.end()); @@ -298,16 +299,34 @@ struct ShellCandidatesCompleter m_candidates.clear(); for (auto c : output | split('\n') | filter([](auto s) { return not s.empty(); })) - m_candidates.emplace_back(c.str(), used_letters(c)); + { + String candidate; + Optional priority; + if (m_flags & Completions::Flags::Priority) + { + priority.emplace(); + std::tie(candidate, *priority) = option_from_string(Meta::Type>{}, c); + if (m_flags & Completions::Flags::Priority and (int)*priority <= 0) + { + String error_message = "error computing shell-script-candidates: priority must be a positive integer"; + write_to_debug_buffer(error_message); + throw runtime_error(std::move(error_message)); + } + } + else + candidate = c.str(); + UsedLetters letters = used_letters(candidate); + m_candidates.push_back(Candidate{std::move(candidate), letters, priority}); + } m_token = token_to_complete; } StringView query = params[token_to_complete].substr(0, pos_in_token); - UsedLetters query_letters = used_letters(query); + RankedMatchQuery q{query, used_letters(query)}; Vector matches; - for (const auto& candidate : m_candidates) + for (const auto& c : m_candidates) { - if (RankedMatch match{candidate.first, candidate.second, query, query_letters}) + if (RankedMatch match{c.candidate, c.used_letters, q, c.priority}) matches.push_back(match); } @@ -327,7 +346,12 @@ struct ShellCandidatesCompleter private: String m_shell_script; - Vector, MemoryDomain::Completion> m_candidates; + struct Candidate { + String candidate; + UsedLetters used_letters; + Optional priority; + }; + Vector m_candidates; int m_token = -1; Completions::Flags m_flags; }; @@ -1204,6 +1228,8 @@ Vector params_to_shell(const ParametersParser& parser) CommandCompleter make_command_completer(StringView type, StringView param, Completions::Flags completions_flags) { + if (completions_flags & Completions::Flags::Priority and type != "shell-script-candidates") + throw runtime_error("-priority requires shell-script-candidates"); if (type == "file") { return [=](const Context& context, CompletionFlags flags, @@ -1279,6 +1305,8 @@ CommandCompleter make_command_completer(StringView type, StringView param, Compl } static CommandCompleter parse_completion_switch(const ParametersParser& parser, Completions::Flags completions_flags) { + if (completions_flags & Completions::Flags::Priority and not parser.get_switch("shell-script-candidates")) + throw runtime_error("-priority requires -shell-script-candidates"); for (StringView completion_switch : {"file-completion", "client-completion", "buffer-completion", "shell-script-completion", "shell-script-candidates", "command-completion", "shell-completion"}) @@ -1294,6 +1322,16 @@ static CommandCompleter parse_completion_switch(const ParametersParser& parser, return {}; } +static Completions::Flags make_completions_flags(const ParametersParser& parser) +{ + Completions::Flags flags = Completions::Flags::None; + if (parser.get_switch("menu")) + flags |= Completions::Flags::Menu; + if (parser.get_switch("priority")) + flags |= Completions::Flags::Priority; + return flags; +} + void define_command(const ParametersParser& parser, Context& context, const ShellContext&) { const String& cmd_name = parser[0]; @@ -1309,9 +1347,6 @@ void define_command(const ParametersParser& parser, Context& context, const Shel if (parser.get_switch("hidden")) flags = CommandFlags::Hidden; - const Completions::Flags completions_flags = parser.get_switch("menu") ? - Completions::Flags::Menu : Completions::Flags::None; - const String& commands = parser[1]; CommandFunc cmd; ParameterDesc desc; @@ -1345,7 +1380,10 @@ void define_command(const ParametersParser& parser, Context& context, const Shel }; } + const Completions::Flags completions_flags = make_completions_flags(parser); CommandCompleter completer = parse_completion_switch(parser, completions_flags); + if (completions_flags & Completions::Flags::Menu and not completer) + throw runtime_error(format("menu switch requires a completion switch", cmd_name)); auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{})); cm.register_command(cmd_name, cmd, docstring, desc, flags, CommandHelper{}, std::move(completer)); @@ -1362,6 +1400,7 @@ const CommandDesc define_command_cmd = { { "hidden", { {}, "do not display the command in completion candidates" } }, { "docstring", { ArgCompleter{}, "define the documentation string for command" } }, { "menu", { {}, "treat completions as the only valid inputs" } }, + { "priority", { {}, "shell script candidates have candidate|priority syntax" } }, { "file-completion", { {}, "complete parameters using filename completion" } }, { "client-completion", { {}, "complete parameters using client name completion" } }, { "buffer-completion", { {}, "complete parameters using buffer name completion" } }, @@ -1429,14 +1468,15 @@ const CommandDesc complete_command_cmd = { "complete-command [] []\n" "define command completion", ParameterDesc{ - { { "menu", { {}, "treat completions as the only valid inputs" } }, }, + { { "menu", { {}, "treat completions as the only valid inputs" } }, + { "priority", { {}, "shell script candidates have candidate|priority syntax" } }, }, ParameterDesc::Flags::None, 2, 3}, CommandFlags::None, CommandHelper{}, make_completer(complete_command_name), [](const ParametersParser& parser, Context& context, const ShellContext&) { - const Completions::Flags flags = parser.get_switch("menu") ? Completions::Flags::Menu : Completions::Flags::None; + const Completions::Flags flags = make_completions_flags(parser); CommandCompleter completer = make_command_completer(parser[1], parser.positional_count() >= 3 ? parser[2] : StringView{}, flags); CommandManager::instance().set_command_completer(parser[0], std::move(completer)); } @@ -2175,6 +2215,7 @@ const CommandDesc prompt_cmd = { { { "init", { ArgCompleter{}, "set initial prompt content" } }, { "password", { {}, "Do not display entered text and clear reg after command" } }, { "menu", { {}, "treat completions as the only valid inputs" } }, + { "priority", { {}, "shell script candidates have candidate|priority syntax" } }, { "file-completion", { {}, "use file completion for prompt" } }, { "client-completion", { {}, "use client completion for prompt" } }, { "buffer-completion", { {}, "use buffer completion for prompt" } }, @@ -2194,9 +2235,7 @@ const CommandDesc prompt_cmd = { const String& command = parser[1]; auto initstr = parser.get_switch("init").value_or(StringView{}); - const Completions::Flags completions_flags = parser.get_switch("menu") ? - Completions::Flags::Menu : Completions::Flags::None; - PromptCompleterAdapter completer = parse_completion_switch(parser, completions_flags); + PromptCompleterAdapter completer = parse_completion_switch(parser, make_completions_flags(parser)); const auto flags = parser.get_switch("password") ? PromptFlags::Password : PromptFlags::None; diff --git a/src/completion.hh b/src/completion.hh index d3aeca2b5f..c5ae4e4ee8 100644 --- a/src/completion.hh +++ b/src/completion.hh @@ -23,7 +23,8 @@ struct Completions None = 0, Quoted = 0b1, Menu = 0b10, - NoEmpty = 0b100 + NoEmpty = 0b100, + Priority = 0b1000 }; constexpr friend bool with_bit_ops(Meta::Type) { return true; } @@ -75,11 +76,11 @@ CandidateList complete(StringView query, ByteCount cursor_pos, static_assert(not std::is_same::value, "complete require long lived strings, not temporaries"); - query = query.substr(0, cursor_pos); + RankedMatchQuery q{query.substr(0, cursor_pos)}; Vector matches; for (const auto& str : container) { - if (RankedMatch match{str, query}) + if (RankedMatch match{str, q}) matches.push_back(match); } std::sort(matches.begin(), matches.end()); diff --git a/src/file.cc b/src/file.cc index c7092209b7..9fddb3011f 100644 --- a/src/file.cc +++ b/src/file.cc @@ -512,10 +512,11 @@ CandidateList complete_filename(StringView prefix, const Regex& ignored_regex, (not only_dirs or S_ISDIR(st.st_mode)); }; auto files = list_files(parsed_dirname, filter); + RankedMatchQuery q{fileprefix}; Vector matches; for (auto& file : files) { - if (RankedMatch match{file, fileprefix}) + if (RankedMatch match{file, q}) matches.push_back(match); } // Hack: when completing directories, also echo back the query if it @@ -524,7 +525,7 @@ CandidateList complete_filename(StringView prefix, const Regex& ignored_regex, 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}); + matches.push_back(RankedMatch{fileprefix, q}); } std::sort(matches.begin(), matches.end()); const bool expand = (flags & FilenameFlags::Expand); @@ -546,10 +547,11 @@ CandidateList complete_command(StringView prefix, ByteCount cursor_pos) return S_ISDIR(st.st_mode) or (S_ISREG(st.st_mode) and executable); }; auto files = list_files(dirname, filter); + RankedMatchQuery q{real_prefix}; Vector matches; for (auto& file : files) { - if (RankedMatch match{file, real_prefix}) + if (RankedMatch match{file, q}) matches.push_back(match); } std::sort(matches.begin(), matches.end()); @@ -565,6 +567,7 @@ CandidateList complete_command(StringView prefix, ByteCount cursor_pos) }; static HashMap command_cache; + RankedMatchQuery q{fileprefix}; Vector matches; for (auto dir : StringView{getenv("PATH")} | split(':')) { @@ -589,7 +592,7 @@ CandidateList complete_command(StringView prefix, ByteCount cursor_pos) } for (auto& cmd : cache.commands) { - if (RankedMatch match{cmd, fileprefix}) + if (RankedMatch match{cmd, q}) matches.push_back(match); } } diff --git a/src/input_handler.cc b/src/input_handler.cc index 69ef338582..957f534a9a 100644 --- a/src/input_handler.cc +++ b/src/input_handler.cc @@ -950,8 +950,9 @@ class Prompt : public InputMode }); else if (key.key == 'w') use_explicit_completer([](const Context& context, StringView token) { + RankedMatchQuery query{token}; CandidateList candidates; - for_n_best(get_word_db(context.buffer()).find_matching(token), + for_n_best(get_word_db(context.buffer()).find_matching(query), 100, [](auto& lhs, auto& rhs){ return rhs < lhs; }, [&](RankedMatch& m) { candidates.push_back(m.candidate().str()); @@ -1082,7 +1083,7 @@ class Prompt : public InputMode items.push_back({ candidate, {} }); const auto menu_style = (m_flags & PromptFlags::Search) ? MenuStyle::Search : MenuStyle::Prompt; - context().client().menu_show(items, {}, menu_style); + context().client().menu_show(std::move(items), {}, menu_style); if (menu) context().client().menu_select(0); diff --git a/src/insert_completer.cc b/src/insert_completer.cc index 83e51d6314..7098ce71b7 100644 --- a/src/insert_completer.cc +++ b/src/insert_completer.cc @@ -123,7 +123,8 @@ InsertCompletion complete_word(const SelectionList& sels, }; auto& word_db = get_word_db(buffer); - Vector matches = word_db.find_matching(prefix) + RankedMatchQuery q{prefix}; + Vector matches = word_db.find_matching(q) | transform([&](auto& m) { return RankedMatchAndBuffer{m, &buffer}; }) | gather(); // Remove words that are being edited @@ -139,7 +140,7 @@ InsertCompletion complete_word(const SelectionList& sels, { if (buf.get() == &buffer or buf->flags() & Buffer::Flags::Debug) continue; - for (auto& m : get_word_db(*buf).find_matching(prefix) | + for (auto& m : get_word_db(*buf).find_matching(q) | // filter out words that are not considered words for the current buffer filter([&](auto& rm) { auto&& c = rm.candidate(); @@ -152,7 +153,7 @@ InsertCompletion complete_word(const SelectionList& sels, using StaticWords = Vector; for (auto& word : options["static_words"].get()) - if (RankedMatch match{word, prefix}) + if (RankedMatch match{word, q}) matches.emplace_back(match, nullptr); unordered_erase(matches, prefix); @@ -295,15 +296,15 @@ InsertCompletion complete_option(const SelectionList& sels, DisplayLine menu_entry; }; - StringView query = buffer.substr(coord, cursor_pos); + RankedMatchQuery query{buffer.substr(coord, cursor_pos)}; Vector matches; for (auto& candidate : opt.list) { - if (RankedMatchAndInfo match{std::get<0>(candidate), query}) + auto& [completion, on_select, menu, priority] = candidate; + if (RankedMatchAndInfo match{completion, query, priority}) { - match.on_select = std::get<1>(candidate); - auto& menu = std::get<2>(candidate); + match.on_select = on_select; match.menu_entry = not menu.empty() ? parse_display_line(expand_tabs(menu, tabstop, column), faces) : DisplayLine{String{}, {}}; diff --git a/src/insert_completer.hh b/src/insert_completer.hh index 269368d50d..fe2ae15f1b 100644 --- a/src/insert_completer.hh +++ b/src/insert_completer.hh @@ -45,7 +45,7 @@ inline StringView option_type_name(Meta::Type) return "completer"; } -using CompletionCandidate = std::tuple; +using CompletionCandidate = std::tuple>; using CompletionList = PrefixedList; inline StringView option_type_name(Meta::Type) diff --git a/src/memory.hh b/src/memory.hh index 6ae64fe9ea..227de5590f 100644 --- a/src/memory.hh +++ b/src/memory.hh @@ -37,6 +37,7 @@ enum class MemoryDomain Events, Completion, Regex, + RankedMatch, Count }; @@ -68,6 +69,7 @@ inline const char* domain_name(MemoryDomain domain) case MemoryDomain::Events: return "Events"; case MemoryDomain::Completion: return "Completion"; case MemoryDomain::Regex: return "Regex"; + case MemoryDomain::RankedMatch: return "RankedMatch"; case MemoryDomain::Count: break; } kak_assert(false); diff --git a/src/normal.cc b/src/normal.cc index 7dc14382f7..249599516a 100644 --- a/src/normal.cc +++ b/src/normal.cc @@ -825,7 +825,8 @@ void regex_prompt(Context& context, String prompt, char reg, T func) }; const auto word = current_word(regex.substr(0_byte, pos)); - auto matches = get_word_db(context.buffer()).find_matching(word); + RankedMatchQuery query{word}; + auto matches = get_word_db(context.buffer()).find_matching(query); constexpr size_t max_count = 100; CandidateList candidates; candidates.reserve(std::min(matches.size(), max_count)); diff --git a/src/option_types.cc b/src/option_types.cc index a1eeb4186a..e32df3b739 100644 --- a/src/option_types.cc +++ b/src/option_types.cc @@ -19,6 +19,11 @@ UnitTest test_option_parsing{[]{ check(Vector{10, 20, 30}, {"10", "20", "30"}); check(HashMap{{"foo", 10}, {"b=r", 20}, {"b:z", 30}}, {"foo=10", "b\\=r=20", "b:z=30"}); check(DebugFlags::Keys | DebugFlags::Hooks, {"hooks|keys"}); + check(std::tuple, Optional>(1, 2, 3), {"1|2|3"}); + std::tuple, Optional> tupleWithNullOptionals{1, {}, {}}; + check(tupleWithNullOptionals, {"1||"}); + // Can also parse if tuple separators are missing. + kak_assert(option_from_strings(Meta::Type{}, {"1"}) == tupleWithNullOptionals); }}; } diff --git a/src/option_types.hh b/src/option_types.hh index 40d367dfb4..7b650e0be5 100644 --- a/src/option_types.hh +++ b/src/option_types.hh @@ -79,6 +79,22 @@ inline Codepoint option_from_string(Meta::Type, StringView str) } constexpr StringView option_type_name(Meta::Type) { return "codepoint"; } +template +String option_to_string(const Optional& opt, Quoting quoting) +{ + if (not opt) + return ""; + return option_to_string(*opt, quoting); +} + +template +Optional option_from_string(Meta::Type>, StringView str) +{ + if (str.empty()) + return {}; + return option_from_string(Meta::Type{}, str); +} + template Vector option_to_strings(const Vector& opt) { @@ -225,8 +241,32 @@ String option_to_string(const std::tuple& opt, Quoting quoting) return option_to_string_impl(quoting, opt, std::make_index_sequence()); } +template +struct IsOptionalImpl : std::false_type{}; +template +struct IsOptionalImpl> : std::true_type{}; + +template +struct CountTrailingOptionals {}; +template<> +struct CountTrailingOptionals<> +{ + static constexpr bool all_optional = true; + static constexpr size_t value = 0; +}; +template +struct CountTrailingOptionals +{ + static constexpr bool all_optional = IsOptionalImpl::value + and CountTrailingOptionals::all_optional; + static_assert(not IsOptionalImpl::value or all_optional, + "non-optional fields cannot follow optional types"); + static constexpr size_t value = all_optional ? 1 + sizeof...(Tail) : CountTrailingOptionals::value; +}; + template -std::tuple option_from_string_impl(Meta::Type>, StringView str, +std::tuple option_from_string_impl(Meta::Type>, + StringView str, std::index_sequence) { struct error : runtime_error @@ -237,15 +277,14 @@ std::tuple option_from_string_impl(Meta::Type>, S }; auto elems = str | split(tuple_separator, '\\') | transform(unescape) - | static_gather(); + | static_gather::value>(); return std::tuple{option_from_string(Meta::Type{}, elems[I])...}; } template std::tuple option_from_string(Meta::Type>, StringView str) { - return option_from_string_impl(Meta::Type>{}, str, - std::make_index_sequence()); + return option_from_string_impl(Meta::Type>{}, str, std::make_index_sequence()); } template diff --git a/src/ranges.hh b/src/ranges.hh index 816d6efbb1..bf9c1cd13d 100644 --- a/src/ranges.hh +++ b/src/ranges.hh @@ -648,36 +648,41 @@ auto gather() }}; } -template +template auto elements() { return ViewFactory{[=] (auto&& range) { using std::begin; using std::end; + using ElementType = std::remove_cvref_t; auto it = begin(range), end_it = end(range); - size_t i = 0; auto elem = [&](size_t index) { - for (; i < index; ++i) - if (++it == end_it) throw ExceptionType{i}; - return *it; + static_assert(sizeof...(Indexes) >= optional_elements); + if (it == end_it) + { + if (index >= sizeof...(Indexes) - optional_elements) + return ElementType{}; + throw ExceptionType{index}; + } + return *it++; }; // Note that initializer lists elements are guaranteed to be sequenced - Array, sizeof...(Indexes)> res{{elem(Indexes)...}}; - if (exact_size and ++it != end_it) - throw ExceptionType{++i}; + Array res{{elem(Indexes)...}}; + if (throw_on_extra_elements and it != end_it) + throw ExceptionType{sizeof...(Indexes)}; return res; }}; } -template +template auto static_gather_impl(std::index_sequence) { - return elements(); + return elements(); } -template +template auto static_gather() { - return static_gather_impl(std::make_index_sequence()); + return static_gather_impl(std::make_index_sequence()); } } diff --git a/src/ranked_match.cc b/src/ranked_match.cc index 873a061310..fad446de7a 100644 --- a/src/ranked_match.cc +++ b/src/ranked_match.cc @@ -1,11 +1,15 @@ #include "ranked_match.hh" #include "flags.hh" +#include "string_utils.hh" #include "unit_tests.hh" #include "utf8_iterator.hh" #include "optional.hh" +#include "vector.hh" #include +#include +#include namespace Kakoune { @@ -34,168 +38,263 @@ bool matches(UsedLetters query, UsedLetters letters) return (query & letters) == query; } -using Utf8It = utf8::iterator; +static bool is_word_boundary(Codepoint prev, Codepoint c) +{ + return (iswalnum((wchar_t)prev) != iswalnum((wchar_t)c)) or + (iswlower((wchar_t)prev) and iswupper((wchar_t)c)); +} -static int count_word_boundaries_match(StringView candidate, StringView query) +static bool is_word_start(Codepoint prev, Codepoint c) { - int count = 0; - Utf8It query_it{query.begin(), query}; - Codepoint prev = 0; - for (Utf8It it{candidate.begin(), candidate}; it != candidate.end(); ++it) - { - const Codepoint c = *it; - const bool is_word_boundary = prev == 0 or - (!iswalnum((wchar_t)prev) and iswalnum((wchar_t)c)) or - (iswlower((wchar_t)prev) and iswupper((wchar_t)c)); - prev = c; + return (not iswalnum((wchar_t)prev) and iswalnum((wchar_t)c)) or + (iswlower((wchar_t)prev) and iswupper((wchar_t)c)); +} - if (not is_word_boundary) - continue; +static bool smartcase_eq(Codepoint candidate_c, Codepoint query_c, + const RankedMatchQuery& query, CharCount query_i) +{ + return candidate_c == query_c or candidate_c == query.smartcase_alternative_match[(size_t)query_i]; +} - const Codepoint lc = to_lower(c); - for (auto qit = query_it; qit != query.end(); ++qit) +static bool greedy_subsequence_match(StringView candidate, const RankedMatchQuery& query) +{ + auto it = candidate.begin(); + CharCount query_i = 0; + for (auto query_it = query.input.begin(); query_it != query.input.end();) + { + if (it == candidate.end()) + return false; + const Codepoint c = utf8::read_codepoint(query_it, query.input.end()); + while (true) { - const Codepoint qc = *qit; - if (qc == (iswlower((wchar_t)qc) ? lc : c)) - { - ++count; - query_it = qit+1; + auto candidate_c = utf8::read_codepoint(it, candidate.end()); + if (smartcase_eq(candidate_c, c, query, query_i)) break; - } + + if (it == candidate.end()) + return false; } - if (query_it == query.end()) - break; + query_i++; } - return count; + return true; } -static bool smartcase_eq(Codepoint candidate, Codepoint query) +static CharCount query_length(const RankedMatchQuery& query) { - return query == (iswlower((wchar_t)query) ? to_lower(candidate) : candidate); + return query.smartcase_alternative_match.size(); } -struct SubseqRes +// Below is an implementation of Gotoh's algorithm for optimal sequence +// alignment following the 1982 paper "An Improved Algorithm for Matching +// Biological Sequences", see +// https://courses.cs.duke.edu/spring21/compsci260/resources/AlignmentPapers/1982.gotoh.pdf +// We simplify the algorithm by always requiring the query to be a subsequence +// of the candidate. +struct Distance { - int max_index; - bool single_word; + // The distance between two strings. + int distance = 0; + // The distance between two strings if the alignment ends in a gap. + int distance_ending_in_gap = 0; }; -static Optional subsequence_match_smart_case(StringView str, StringView subseq) +template +class SubsequenceDistance { - bool single_word = true; - int max_index = -1; - auto it = str.begin(); - int index = 0; - for (auto subseq_it = subseq.begin(); subseq_it != subseq.end();) +public: + SubsequenceDistance(const RankedMatchQuery& query, StringView candidate) + : query{query}, candidate{candidate}, + stride{candidate.char_length() + 1}, + m_matrix{(size_t)( + (full_matrix ? (query_length(query) + 1) : 2) + * stride)} {} + + ArrayView operator[](CharCount query_i) { - if (it == str.end()) - return {}; - const Codepoint c = utf8::read_codepoint(subseq_it, subseq.end()); - while (true) + return {m_matrix.data() + (size_t)(query_i * stride), stride}; + } + ConstArrayView operator[](CharCount query_i) const + { + return {m_matrix.data() + (size_t)(query_i * stride), stride}; + } + + // The index of the last matched character. + CharCount max_index = 0; + // These fields exist to allow pretty-printing in GDB. + const RankedMatchQuery& query; + const StringView candidate; +private: + CharCount stride; + // For each combination of prefixes of candidate and query, this holds + // their distance. For example, (*this)[2][3] holds the distance between + // the first two query characters and the first three characters from + // the candidate. + Vector m_matrix; +}; + +static constexpr int infinity = std::numeric_limits::max(); +constexpr int max_index_weight = 1; + +template +static SubsequenceDistance subsequence_distance(const RankedMatchQuery& query, StringView candidate) +{ + auto match_bonus = [](bool starts_word, bool is_same_case) -> int { + return -150 * starts_word + -40 + -4 * is_same_case; + }; + constexpr int gap_weight = 200; + constexpr int gap_extend_weight = 1; + + SubsequenceDistance distance{query, candidate}; + + CharCount candidate_length = candidate.char_length(); + + // Compute the distance of skipping a prefix of the candidate. + for (CharCount candidate_i = 0; candidate_i <= candidate_length; candidate_i++) + distance[0][candidate_i].distance = (int)candidate_i * gap_extend_weight; + + CharCount query_i, candidate_i; + String::const_iterator query_it, candidate_it; + for (query_i = 1, query_it = query.input.begin(); + query_i <= query_length(query); + query_i++) + { + CharCount query_virtual_i = query_i; + CharCount prev_query_virtual_i = query_i - 1; + // Only keep the last two rows in memory, swapping them in each iteration. + if constexpr (not full_matrix) { - auto str_c = utf8::read_codepoint(it, str.end()); - if (smartcase_eq(str_c, c)) - break; + query_virtual_i %= 2; + prev_query_virtual_i %= 2; + } - if (max_index != -1 and single_word and not is_word(str_c)) - single_word = false; + auto row = distance[query_virtual_i]; + auto prev_row = distance[prev_query_virtual_i]; - ++index; - if (it == str.end()) - return {}; + // Since we only allow subsequence matches, we don't need deletions. + // This rules out prefix-matches where the query is longer than the + // candidate. Mark them as impossible. We only need to mark the boundary + // cases since we never read others. + if (query_i - 1 <= candidate_length) + { + row[query_i - 1].distance = infinity; + row[query_i - 1].distance_ending_in_gap = infinity; + } + Codepoint query_c = utf8::read_codepoint(query_it, query.input.end()); + Codepoint prev_c; + // Since we don't allow deletions, the candidate must be at least + // as long as the query. This allows us to skip some cells. + for (candidate_i = query_i, + candidate_it = utf8::advance(candidate.begin(), candidate.end(), query_i -1), + prev_c = utf8::prev_codepoint(candidate_it, candidate.begin()).value_or(Codepoint(0)); + candidate_i <= candidate_length; + candidate_i++) + { + Codepoint candidate_c = utf8::read_codepoint(candidate_it, candidate.end()); + + int distance_ending_in_gap = infinity; + if (auto parent = row[candidate_i - 1]; parent.distance != infinity) + { + bool is_trailing_gap = query_i == query_length(query); + int start_gap = parent.distance + (gap_weight * not is_trailing_gap) + gap_extend_weight; + int extend_gap = parent.distance_ending_in_gap == infinity + ? infinity + : parent.distance_ending_in_gap + gap_extend_weight; + distance_ending_in_gap = std::min(start_gap, extend_gap); + } + + int distance_match = infinity; + if (Distance parent = prev_row[candidate_i - 1]; + parent.distance != infinity and smartcase_eq(candidate_c, query_c, query, query_i - 1)) + { + bool starts_word = is_word_start(prev_c, candidate_c); + bool is_same_case = candidate_c == query_c; + distance_match = parent.distance + match_bonus(starts_word, is_same_case); + } + + row[candidate_i].distance = std::min(distance_match, distance_ending_in_gap); + row[candidate_i].distance_ending_in_gap = distance_ending_in_gap; + if (query_i == query_length(query) and distance_match < distance_ending_in_gap) + distance.max_index = candidate_i - 1; + prev_c = candidate_c; } - max_index = index++; } - return SubseqRes{max_index, single_word}; + return distance; } +RankedMatchQuery::RankedMatchQuery(StringView query) : RankedMatchQuery(query, {}) {} + +RankedMatchQuery::RankedMatchQuery(StringView input, UsedLetters used_letters) + : input(input), used_letters(used_letters), + smartcase_alternative_match(input | transform([](Codepoint c) -> Optional { + if (is_lower(c)) + return to_upper(c); + return {}; + }) | gather()) {} + template -RankedMatch::RankedMatch(StringView candidate, StringView query, TestFunc func) +RankedMatch::RankedMatch(StringView candidate, const RankedMatchQuery& query, Optional priority, TestFunc func) { - if (query.length() > candidate.length()) + if (query.input.length() > candidate.length()) return; - if (query.empty()) + if (query.input.empty()) { m_candidate = candidate; m_matches = true; + if (priority) + m_distance = *priority; return; } if (not func()) return; - auto res = subsequence_match_smart_case(candidate, query); - if (not res) + // Our matching is quadratic; avoid a hypothetical blowup by only looking at a prefix. + constexpr CharCount candidate_max_length = 1000; + StringView bounded_candidate = candidate.char_length() > candidate_max_length + ? candidate.substr(0, candidate_max_length) + : candidate; + + if (not greedy_subsequence_match(bounded_candidate, query)) return; m_candidate = candidate; m_matches = true; - m_max_index = res->max_index; - if (res->single_word) - m_flags |= Flags::SingleWord; - if (smartcase_eq(candidate[0], query[0])) - m_flags |= Flags::FirstCharMatch; + auto distance = subsequence_distance(query, bounded_candidate); - auto it = std::search(candidate.begin(), candidate.end(), - query.begin(), query.end(), smartcase_eq); - if (it != candidate.end()) + m_distance = distance[query_length(query) % 2][bounded_candidate.char_length()].distance + + (int)distance.max_index * max_index_weight; + if (priority) { - m_flags |= Flags::Contiguous; - if (it == candidate.begin()) - { - m_flags |= Flags::Prefix; - if (query.length() == candidate.length()) - { - m_flags |= Flags::SmartFullMatch; - if (candidate == query) - m_flags |= Flags::FullMatch; - } - } + double effective_priority = *priority; + if (auto query_len = query.input.char_length(); query_len != 0) + effective_priority = std::pow(effective_priority, 1.0 / (double)(size_t)query_len); + m_distance = m_distance >= 0 ? (m_distance * effective_priority) : (m_distance / effective_priority); } - - m_word_boundary_match_count = count_word_boundaries_match(candidate, query); - if (m_word_boundary_match_count == query.length()) - m_flags |= Flags::OnlyWordBoundary; } RankedMatch::RankedMatch(StringView candidate, UsedLetters candidate_letters, - StringView query, UsedLetters query_letters) - : RankedMatch{candidate, query, [&] { - return matches(to_lower(query_letters), to_lower(candidate_letters)) and - matches(query_letters & upper_mask, candidate_letters & upper_mask); + const RankedMatchQuery& query, Optional priority) + : RankedMatch{candidate, query, priority, [&] { + return matches(to_lower(query.used_letters), to_lower(candidate_letters)) and + matches(query.used_letters & upper_mask, candidate_letters & upper_mask); }} {} -RankedMatch::RankedMatch(StringView candidate, StringView query) - : RankedMatch{candidate, query, [] { return true; }} -{ -} - -static bool is_word_boundary(Codepoint prev, Codepoint c) +RankedMatch::RankedMatch(StringView candidate, const RankedMatchQuery& query, Optional priority) + : RankedMatch{candidate, query, priority, [] { return true; }} { - return (iswalnum((wchar_t)prev)) != iswalnum((wchar_t)c) or - (iswlower((wchar_t)prev) != iswlower((wchar_t)c)); } bool RankedMatch::operator<(const RankedMatch& other) const { kak_assert((bool)*this and (bool)other); - const auto diff = m_flags ^ other.m_flags; - // flags are different, use their ordering to return the first match - if (diff != Flags::None) - return (int)(m_flags & diff) > (int)(other.m_flags & diff); - - // If we are SingleWord, FirstCharMatch will do the job, and we dont want to take - // other words boundaries into account. - if (not (m_flags & (Flags::Prefix | Flags::SingleWord)) and - m_word_boundary_match_count != other.m_word_boundary_match_count) - return m_word_boundary_match_count > other.m_word_boundary_match_count; - - if (m_max_index != other.m_max_index) - return m_max_index < other.m_max_index; + if (m_distance != other.m_distance) + return m_distance < other.m_distance; // Reorder codepoints to improve matching behaviour auto order = [](Codepoint cp) { return cp == '/' ? 0 : cp; }; @@ -239,28 +338,123 @@ bool RankedMatch::operator<(const RankedMatch& other) const } } +// returns the base-2 logarithm, rounded down +constexpr uint32_t log2(uint32_t n) noexcept +{ + return 31 - std::countl_zero(n); +} + +static_assert(log2(1) == 0); +static_assert(log2(2) == 1); +static_assert(log2(3) == 1); +static_assert(log2(4) == 2); + +// returns a string representation of the distance matrix, for debugging only +[[maybe_unused]] static String to_string(const SubsequenceDistance& distance) +{ + const RankedMatchQuery& query = distance.query; + StringView candidate = distance.candidate; + + auto candidate_length = candidate.char_length(); + + int distance_amplitude = 1; + for (auto query_i = 0; query_i <= query_length(query); query_i++) + for (auto candidate_i = 1; candidate_i <= candidate_length; candidate_i++) + if (distance[query_i][candidate_i].distance != infinity) + distance_amplitude = std::max(distance_amplitude, std::abs(distance[query_i][candidate_i].distance)); + ColumnCount max_digits = log2(distance_amplitude) / log2(10) + 1; + ColumnCount cell_width = 2 * (max_digits + 1) // two numbers with a minus sign + + 2; // separator between the numbers, plus one space + + String s = "\n"; + auto query_it = query.input.begin(); + s += String{' ', cell_width}; + for (auto query_i = 0; query_i <= query_length(query); query_i++) + { + Codepoint query_c = query_i == 0 ? ' ' : utf8::read_codepoint(query_it, query.input.end()); + s += left_pad(to_string(query_c), cell_width); + } + s += "\n"; + + auto candidate_it = candidate.begin(); + for (CharCount candidate_i = 0; candidate_i <= candidate_length; candidate_i++) + { + Codepoint candidate_c = candidate_i == 0 ? ' ' : utf8::read_codepoint(candidate_it, candidate.end()); + s += left_pad(to_string(candidate_c), cell_width); + for (CharCount query_i = 0; query_i <= query_length(query); query_i++) + { + auto distance_to_string = [](int d) -> String { + return d == infinity ? String{"∞"} : to_string(d); + }; + Distance cell = distance[query_i][candidate_i]; + s += left_pad( + distance_to_string(cell.distance) + "/" + distance_to_string(cell.distance_ending_in_gap), + cell_width); + } + s += "\n"; + } + + return s; +} + UnitTest test_ranked_match{[] { - kak_assert(count_word_boundaries_match("run_all_tests", "rat") == 3); - kak_assert(count_word_boundaries_match("run_all_tests", "at") == 2); - kak_assert(count_word_boundaries_match("countWordBoundariesMatch", "wm") == 2); - kak_assert(count_word_boundaries_match("countWordBoundariesMatch", "cobm") == 3); - kak_assert(count_word_boundaries_match("countWordBoundariesMatch", "cWBM") == 4); - kak_assert(RankedMatch{"source", "so"} < RankedMatch{"source_data", "so"}); - kak_assert(not (RankedMatch{"source_data", "so"} < RankedMatch{"source", "so"})); - kak_assert(not (RankedMatch{"source", "so"} < RankedMatch{"source", "so"})); - kak_assert(RankedMatch{"single/word", "wo"} < RankedMatch{"multiw/ord", "wo"}); - kak_assert(RankedMatch{"foo/bar/foobar", "foobar"} < RankedMatch{"foo/bar/baz", "foobar"}); - kak_assert(RankedMatch{"delete-buffer", "db"} < RankedMatch{"debug", "db"}); - kak_assert(RankedMatch{"create_task", "ct"} < RankedMatch{"constructor", "ct"}); - kak_assert(RankedMatch{"class", "cla"} < RankedMatch{"class::attr", "cla"}); - kak_assert(RankedMatch{"meta/", "meta"} < RankedMatch{"meta-a/", "meta"}); - kak_assert(RankedMatch{"find(1p)", "find"} < RankedMatch{"findfs(8)", "find"}); - kak_assert(RankedMatch{"find(1p)", "fin"} < RankedMatch{"findfs(8)", "fin"}); - kak_assert(RankedMatch{"sys_find(1p)", "sys_find"} < RankedMatch{"sys_findfs(8)", "sys_find"}); - kak_assert(RankedMatch{"init", ""} < RankedMatch{"__init__", ""}); - kak_assert(RankedMatch{"init", "ini"} < RankedMatch{"__init__", "ini"}); - kak_assert(RankedMatch{"a", ""} < RankedMatch{"b", ""}); - kak_assert(RankedMatch{"expresions", "expresins"} < RankedMatch{"expressionism's", "expresins"}); + // Convenience variables, for debugging only. + Optional q; + Optional> distance_better; + Optional> distance_worse; + + auto ranked_match_order = [&](StringView query, StringView better, StringView worse) -> bool { + q = RankedMatchQuery{query}; + distance_better = subsequence_distance(*q, better); + distance_worse = subsequence_distance(*q, worse); + return RankedMatch{better, *q} < RankedMatch{worse, *q}; + }; + + kak_assert(ranked_match_order("so", "source", "source_data")); + kak_assert(not ranked_match_order("so", "source_data", "source")); + kak_assert(not ranked_match_order("so", "source", "source")); + kak_assert(ranked_match_order("wo", "single/word", "multiw/ord")); + kak_assert(ranked_match_order("foobar", "foo/bar/foobar", "foo/bar/baz")); + kak_assert(ranked_match_order("db", "delete-buffer", "debug")); + kak_assert(ranked_match_order("ct", "create_task", "constructor")); + kak_assert(ranked_match_order("cla", "class", "class::attr")); + kak_assert(ranked_match_order("meta", "meta/", "meta-a/")); + kak_assert(ranked_match_order("find", "find(1p)", "findfs(8)")); + kak_assert(ranked_match_order("fin", "find(1p)", "findfs(8)")); + kak_assert(ranked_match_order("sys_find", "sys_find(1p)", "sys_findfs(8)")); + kak_assert(ranked_match_order("", "init", "__init__")); + kak_assert(ranked_match_order("ini", "init", "__init__")); + kak_assert(ranked_match_order("", "a", "b")); + kak_assert(ranked_match_order("expresins", "expresions", "expressionism's")); + kak_assert(ranked_match_order("gre", "*grep*", ".git/rebase-merge/git-rebase-todo")); + kak_assert(ranked_match_order("CAPRAN", "CAPABILITY_RANGE_FORMATTING", "CAPABILITY_SELECTION_RANGE")); + kak_assert(ranked_match_order("mal", "malt", "formal")); + kak_assert(ranked_match_order("fa", "face", "find-apply-changes")); + kak_assert(ranked_match_order("cne", "cargo-next-error", "comment-line")); + kak_assert(ranked_match_order("cne", "cargo-next-error", "ccls-navigate")); + kak_assert(ranked_match_order("cpe", "cargo-previous-error", "cpp-alternative-file")); + kak_assert(ranked_match_order("server_", "server_capabilities", "SERVER_CANCELLED")); + kak_assert(ranked_match_order("server_", "server_capabilities_capabilities", "SERVER_CANCELLED")); + kak_assert(ranked_match_order("codegen", "clang/test/CodeGen/asm.c", "clang/test/ASTMerge/codegen-body/test.c")); + kak_assert(ranked_match_order("cho", "tchou kanaky", "tachou kanay")); // Prefer the leftmost match. + kak_assert(ranked_match_order("clang-query", "clang/tools/clang-query/ClangQuery.cpp", "clang/test/Tooling/clang-query.cpp")); + kak_assert(ranked_match_order("clangd", "clang-tools-extra/clangd/README.md", "clang/docs/conf.py")); + kak_assert(ranked_match_order("rm.cc", "ranked_match.cc", "remote.cc")); + kak_assert(ranked_match_order("rm.cc", "src/ranked_match.cc", "test/README.asciidoc")); + kak_assert(ranked_match_order("fooo", "foo.o", "fo.o.o")); + kak_assert(ranked_match_order("evilcorp-lint/bar.go", "scripts/evilcorp-lint/foo/bar.go", "src/evilcorp-client/foo/bar.go")); + kak_assert(ranked_match_order("lang/haystack/needle.c", "git.evilcorp.com/language/haystack/aaa/needle.c", "git.evilcorp.com/aaa/ng/wrong-haystack/needle.cpp")); + + auto ranked_match_order_with_priority = [&](StringView query, StringView better, Priority better_priority, StringView worse, Priority worse_priority) -> bool { + q = RankedMatchQuery{query}; + distance_better = subsequence_distance(*q, better); + distance_worse = subsequence_distance(*q, worse); + return RankedMatch{better, *q, better_priority} < RankedMatch{worse, *q, worse_priority}; + }; + + kak_assert(ranked_match_order_with_priority("", "Qualify as `std::collections::HashMap`", 1, "Extract type as type alias", 2)); + kak_assert(ranked_match_order_with_priority("as", "Qualify as `std::collections::HashMap`", 1, "Extract type as type alias", 2)); + }}; UnitTest test_used_letters{[]() diff --git a/src/ranked_match.hh b/src/ranked_match.hh index 62d6b8f033..98ab9f477d 100644 --- a/src/ranked_match.hh +++ b/src/ranked_match.hh @@ -3,6 +3,7 @@ #include "string.hh" #include "meta.hh" +#include "vector.hh" #include @@ -19,11 +20,25 @@ inline UsedLetters to_lower(UsedLetters letters) return ((letters & upper_mask) >> 26) | (letters & (~upper_mask)); } +struct RankedMatchQuery +{ + const StringView input; + const UsedLetters used_letters; + // For each lowercase character in the input, this holds the corresponding + // uppercase character. + const Vector> smartcase_alternative_match; + + explicit RankedMatchQuery(StringView query); + explicit RankedMatchQuery(StringView query, UsedLetters used_letters); +}; + +using Priority = size_t; + struct RankedMatch { - RankedMatch(StringView candidate, StringView query); + RankedMatch(StringView candidate, const RankedMatchQuery& query, Optional priority = {}); RankedMatch(StringView candidate, UsedLetters candidate_letters, - StringView query, UsedLetters query_letters); + const RankedMatchQuery& query, Optional priority = {}); const StringView& candidate() const { return m_candidate; } bool operator<(const RankedMatch& other) const; @@ -33,27 +48,11 @@ struct RankedMatch private: template - RankedMatch(StringView candidate, StringView query, TestFunc test); - - enum class Flags : int - { - None = 0, - // Order is important, the highest bit has precedence for comparison - FirstCharMatch = 1 << 0, - SingleWord = 1 << 1, - Contiguous = 1 << 2, - OnlyWordBoundary = 1 << 3, - Prefix = 1 << 4, - SmartFullMatch = 1 << 5, - FullMatch = 1 << 6, - }; - friend constexpr bool with_bit_ops(Meta::Type) { return true; } + RankedMatch(StringView candidate, const RankedMatchQuery& query, Optional priority, TestFunc test); StringView m_candidate{}; bool m_matches = false; - Flags m_flags = Flags::None; - int m_word_boundary_match_count = 0; - int m_max_index = 0; + int m_distance = 0; }; } diff --git a/src/word_db.cc b/src/word_db.cc index 56b7fedf73..688e65f875 100644 --- a/src/word_db.cc +++ b/src/word_db.cc @@ -202,14 +202,13 @@ int WordDB::get_word_occurences(StringView word) const return 0; } -RankedMatchList WordDB::find_matching(StringView query) +RankedMatchList WordDB::find_matching(const RankedMatchQuery& query) { update_db(); - const UsedLetters letters = used_letters(query); RankedMatchList res; for (auto&& word : m_words) { - if (RankedMatch match{word.key, word.value.letters, query, letters}) + if (RankedMatch match{word.key, word.value.letters, query}) res.push_back(match); } @@ -236,17 +235,18 @@ UnitTest test_word_db{[]() Buffer buffer("test", Buffer::Flags::None, make_lines("tchou mutch\n", "tchou kanaky tchou\n", "\n", "tchaa tchaa\n", "allo\n")); WordDB word_db(buffer); - auto res = word_db.find_matching(""); + RankedMatchQuery query{""}; + auto res = word_db.find_matching(query); std::sort(res.begin(), res.end(), cmp_words); kak_assert(eq(res, WordList{ "allo", "kanaky", "mutch", "tchaa", "tchou" })); kak_assert(word_db.get_word_occurences("tchou") == 3); kak_assert(word_db.get_word_occurences("allo") == 1); buffer.erase({1, 6}, {4, 0}); - res = word_db.find_matching(""); + res = word_db.find_matching(query); std::sort(res.begin(), res.end(), cmp_words); kak_assert(eq(res, WordList{ "allo", "mutch", "tchou" })); buffer.insert({1, 0}, "re"); - res = word_db.find_matching(""); + res = word_db.find_matching(query); std::sort(res.begin(), res.end(), cmp_words); kak_assert(eq(res, WordList{ "allo", "mutch", "retchou", "tchou" })); }}; diff --git a/src/word_db.hh b/src/word_db.hh index ac7b8c01e5..88302c006e 100644 --- a/src/word_db.hh +++ b/src/word_db.hh @@ -25,7 +25,7 @@ public: WordDB(const WordDB&) = delete; WordDB(WordDB&&) noexcept; - RankedMatchList find_matching(StringView str); + RankedMatchList find_matching(const RankedMatchQuery& query); int get_word_occurences(StringView word) const; private: