Skip to content

Commit 4ae738f

Browse files
committed
[custom-completion-sorting] Support opt-in priority field in shell script candidates
This is inspired by a recent change in [Helix] that fixes sorting of code actions. We have the same problem because kak-lsp uses ":prompt -shell-script-candidates" to show code actions. For example, on this Rust file: fn main() { let f: FnOnce(HashMap<i32, i32>); } with the cursor on "HashMap", a ":lsp-code-actions" will offer two code actions (from rust-analyzer): Extract type as type alias" Import `std::collections::HashMap` The first one is a refactoring and the second one is a quickfix. If fuzzy match scores are equal, Kakoune sorts completions lexicographically, which is suboptimal because the user will almost always want to run the quickfix first. Allow users to influence the order via a new "-priority" switch. When this switch is used, Kakoune expects a second field in shell-script-candidates completions, like so: Extract type as type alias"|2 Import `std::collections::HashMap`|1 The priority field is taken into account when computing fuzzy match scores. Due to the lack of test cases, the math to do so does not have a solid footing yet. Here's how it works for now. - "distance" is the fuzzy match score (lower is better) - "priority" is the new user-specificed ranking, a positive integer (lower is better) - "query_length" is the length of the string that is used to filter completions effective_priority = priority ^ (1 / query_length) if query_length != 0 else priority prioritized_distance = distance * (effective_priority ^ sign(distance)) The ideas are that 1. A priority of 1 is neutral. Higher values increase the distance (making it worse). 2. The longer the query, the lower the impact of "priority". --- Used by kakoune-lsp/kakoune-lsp#657 [Helix]: helix-editor/helix#4134 Part of mawww#1709
1 parent 78570b6 commit 4ae738f

File tree

4 files changed

+79
-23
lines changed

4 files changed

+79
-23
lines changed

src/commands.cc

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -299,16 +299,34 @@ struct ShellCandidatesCompleter
299299
m_candidates.clear();
300300
for (auto c : output | split<StringView>('\n')
301301
| filter([](auto s) { return not s.empty(); }))
302-
m_candidates.emplace_back(c.str(), used_letters(c));
302+
{
303+
String candidate;
304+
Optional<Priority> priority;
305+
if (m_flags & Completions::Flags::Priority)
306+
{
307+
priority.emplace();
308+
std::tie(candidate, *priority) = option_from_string(Meta::Type<std::tuple<String, Priority>>{}, c);
309+
if (m_flags & Completions::Flags::Priority and (int)*priority <= 0)
310+
{
311+
String error_message = "error computing shell-script-candidates: priority must be a positive integer";
312+
write_to_debug_buffer(error_message);
313+
throw runtime_error(std::move(error_message));
314+
}
315+
}
316+
else
317+
candidate = c.str();
318+
UsedLetters letters = used_letters(candidate);
319+
m_candidates.push_back(Candidate{std::move(candidate), letters, priority});
320+
}
303321
m_token = token_to_complete;
304322
}
305323

306324
StringView query = params[token_to_complete].substr(0, pos_in_token);
307325
RankedMatchQuery q{query, used_letters(query)};
308326
Vector<RankedMatch> matches;
309-
for (const auto& candidate : m_candidates)
327+
for (const auto& c : m_candidates)
310328
{
311-
if (RankedMatch match{candidate.first, candidate.second, q})
329+
if (RankedMatch match{c.candidate, c.used_letters, q, c.priority})
312330
matches.push_back(match);
313331
}
314332

@@ -328,7 +346,12 @@ struct ShellCandidatesCompleter
328346

329347
private:
330348
String m_shell_script;
331-
Vector<std::pair<String, UsedLetters>, MemoryDomain::Completion> m_candidates;
349+
struct Candidate {
350+
String candidate;
351+
UsedLetters used_letters;
352+
Optional<Priority> priority;
353+
};
354+
Vector<Candidate, MemoryDomain::Completion> m_candidates;
332355
int m_token = -1;
333356
Completions::Flags m_flags;
334357
};
@@ -1208,6 +1231,8 @@ Vector<String> params_to_shell(const ParametersParser& parser)
12081231

12091232
CommandCompleter make_command_completer(StringView type, StringView param, Completions::Flags completions_flags)
12101233
{
1234+
if (completions_flags & Completions::Flags::Priority and type != "shell-script-candidates")
1235+
throw runtime_error("-priority requires shell-script-candidates");
12111236
if (type == "file")
12121237
{
12131238
return [=](const Context& context, CompletionFlags flags,
@@ -1283,6 +1308,8 @@ CommandCompleter make_command_completer(StringView type, StringView param, Compl
12831308
}
12841309

12851310
static CommandCompleter parse_completion_switch(const ParametersParser& parser, Completions::Flags completions_flags) {
1311+
if (completions_flags & Completions::Flags::Priority and not parser.get_switch("shell-script-candidates"))
1312+
throw runtime_error("-priority requires -shell-script-candidates");
12861313
for (StringView completion_switch : {"file-completion", "client-completion", "buffer-completion",
12871314
"shell-script-completion", "shell-script-candidates",
12881315
"command-completion", "shell-completion"})
@@ -1298,6 +1325,16 @@ static CommandCompleter parse_completion_switch(const ParametersParser& parser,
12981325
return {};
12991326
}
13001327

1328+
static Completions::Flags make_completions_flags(const ParametersParser& parser)
1329+
{
1330+
Completions::Flags flags = Completions::Flags::None;
1331+
if (parser.get_switch("menu"))
1332+
flags |= Completions::Flags::Menu;
1333+
if (parser.get_switch("priority"))
1334+
flags |= Completions::Flags::Priority;
1335+
return flags;
1336+
}
1337+
13011338
void define_command(const ParametersParser& parser, Context& context, const ShellContext&)
13021339
{
13031340
const String& cmd_name = parser[0];
@@ -1313,10 +1350,6 @@ void define_command(const ParametersParser& parser, Context& context, const Shel
13131350
if (parser.get_switch("hidden"))
13141351
flags = CommandFlags::Hidden;
13151352

1316-
bool menu = (bool)parser.get_switch("menu");
1317-
const Completions::Flags completions_flags = menu ?
1318-
Completions::Flags::Menu : Completions::Flags::None;
1319-
13201353
const String& commands = parser[1];
13211354
CommandFunc cmd;
13221355
ParameterDesc desc;
@@ -1350,8 +1383,9 @@ void define_command(const ParametersParser& parser, Context& context, const Shel
13501383
};
13511384
}
13521385

1386+
const Completions::Flags completions_flags = make_completions_flags(parser);
13531387
CommandCompleter completer = parse_completion_switch(parser, completions_flags);
1354-
if (menu and not completer)
1388+
if (completions_flags & Completions::Flags::Menu and not completer)
13551389
throw runtime_error(format("menu switch requires a completion switch", cmd_name));
13561390
auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{}));
13571391

@@ -1369,6 +1403,7 @@ const CommandDesc define_command_cmd = {
13691403
{ "hidden", { {}, "do not display the command in completion candidates" } },
13701404
{ "docstring", { ArgCompleter{}, "define the documentation string for command" } },
13711405
{ "menu", { {}, "treat completions as the only valid inputs" } },
1406+
{ "priority", { {}, "shell script candidates have candidate|priority syntax" } },
13721407
{ "file-completion", { {}, "complete parameters using filename completion" } },
13731408
{ "client-completion", { {}, "complete parameters using client name completion" } },
13741409
{ "buffer-completion", { {}, "complete parameters using buffer name completion" } },
@@ -1436,14 +1471,15 @@ const CommandDesc complete_command_cmd = {
14361471
"complete-command [<switches>] <name> <type> [<param>]\n"
14371472
"define command completion",
14381473
ParameterDesc{
1439-
{ { "menu", { {}, "treat completions as the only valid inputs" } }, },
1474+
{ { "menu", { {}, "treat completions as the only valid inputs" } },
1475+
{ "priority", { {}, "shell script candidates have candidate|priority syntax" } }, },
14401476
ParameterDesc::Flags::None, 2, 3},
14411477
CommandFlags::None,
14421478
CommandHelper{},
14431479
make_completer(complete_command_name),
14441480
[](const ParametersParser& parser, Context& context, const ShellContext&)
14451481
{
1446-
const Completions::Flags flags = parser.get_switch("menu") ? Completions::Flags::Menu : Completions::Flags::None;
1482+
const Completions::Flags flags = make_completions_flags(parser);
14471483
CommandCompleter completer = make_command_completer(parser[1], parser.positional_count() >= 3 ? parser[2] : StringView{}, flags);
14481484
CommandManager::instance().set_command_completer(parser[0], std::move(completer));
14491485
}
@@ -2186,6 +2222,7 @@ const CommandDesc prompt_cmd = {
21862222
{ { "init", { ArgCompleter{}, "set initial prompt content" } },
21872223
{ "password", { {}, "Do not display entered text and clear reg after command" } },
21882224
{ "menu", { {}, "treat completions as the only valid inputs" } },
2225+
{ "priority", { {}, "shell script candidates have candidate|priority syntax" } },
21892226
{ "file-completion", { {}, "use file completion for prompt" } },
21902227
{ "client-completion", { {}, "use client completion for prompt" } },
21912228
{ "buffer-completion", { {}, "use buffer completion for prompt" } },
@@ -2205,9 +2242,7 @@ const CommandDesc prompt_cmd = {
22052242
const String& command = parser[1];
22062243
auto initstr = parser.get_switch("init").value_or(StringView{});
22072244

2208-
const Completions::Flags completions_flags = parser.get_switch("menu") ?
2209-
Completions::Flags::Menu : Completions::Flags::None;
2210-
PromptCompleterAdapter completer = parse_completion_switch(parser, completions_flags);
2245+
PromptCompleterAdapter completer = parse_completion_switch(parser, make_completions_flags(parser));
22112246

22122247
const auto flags = parser.get_switch("password") ?
22132248
PromptFlags::Password : PromptFlags::None;

src/completion.hh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ struct Completions
2323
None = 0,
2424
Quoted = 0b1,
2525
Menu = 0b10,
26-
NoEmpty = 0b100
26+
NoEmpty = 0b100,
27+
Priority = 0b1000
2728
};
2829

2930
constexpr friend bool with_bit_ops(Meta::Type<Flags>) { return true; }

src/ranked_match.cc

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ RankedMatchQuery::RankedMatchQuery(StringView input, UsedLetters used_letters)
234234
}) | gather<decltype(smartcase_alternative_match)>()) {}
235235

236236
template<typename TestFunc>
237-
RankedMatch::RankedMatch(StringView candidate, const RankedMatchQuery& query, TestFunc func)
237+
RankedMatch::RankedMatch(StringView candidate, const RankedMatchQuery& query, Optional<Priority> priority, TestFunc func)
238238
{
239239
if (query.input.length() > candidate.length())
240240
return;
@@ -243,6 +243,8 @@ RankedMatch::RankedMatch(StringView candidate, const RankedMatchQuery& query, Te
243243
{
244244
m_candidate = candidate;
245245
m_matches = true;
246+
if (priority)
247+
m_distance = *priority;
246248
return;
247249
}
248250

@@ -265,18 +267,25 @@ RankedMatch::RankedMatch(StringView candidate, const RankedMatchQuery& query, Te
265267

266268
m_distance = distance[query_length(query) % 2][bounded_candidate.char_length()].distance
267269
+ (int)distance.max_index * max_index_weight;
270+
if (priority)
271+
{
272+
double effective_priority = *priority;
273+
if (auto query_len = query.input.char_length(); query_len != 0)
274+
effective_priority = std::pow(effective_priority, 1.0 / (double)(size_t)query_len);
275+
m_distance = m_distance >= 0 ? (m_distance * effective_priority) : (m_distance / effective_priority);
276+
}
268277
}
269278

270279
RankedMatch::RankedMatch(StringView candidate, UsedLetters candidate_letters,
271-
const RankedMatchQuery& query)
272-
: RankedMatch{candidate, query, [&] {
280+
const RankedMatchQuery& query, Optional<Priority> priority)
281+
: RankedMatch{candidate, query, priority, [&] {
273282
return matches(to_lower(query.used_letters), to_lower(candidate_letters)) and
274283
matches(query.used_letters & upper_mask, candidate_letters & upper_mask);
275284
}} {}
276285

277286

278-
RankedMatch::RankedMatch(StringView candidate, const RankedMatchQuery& query)
279-
: RankedMatch{candidate, query, [] { return true; }}
287+
RankedMatch::RankedMatch(StringView candidate, const RankedMatchQuery& query, Optional<Priority> priority)
288+
: RankedMatch{candidate, query, priority, [] { return true; }}
280289
{
281290
}
282291

@@ -435,6 +444,17 @@ UnitTest test_ranked_match{[] {
435444
kak_assert(ranked_match_order("fooo", "foo.o", "fo.o.o"));
436445
kak_assert(ranked_match_order("evilcorp-lint/bar.go", "scripts/evilcorp-lint/foo/bar.go", "src/evilcorp-client/foo/bar.go"));
437446
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"));
447+
448+
auto ranked_match_order_with_priority = [&](StringView query, StringView better, Priority better_priority, StringView worse, Priority worse_priority) -> bool {
449+
q = RankedMatchQuery{query};
450+
distance_better = subsequence_distance<true>(*q, better);
451+
distance_worse = subsequence_distance<true>(*q, worse);
452+
return RankedMatch{better, *q, better_priority} < RankedMatch{worse, *q, worse_priority};
453+
};
454+
455+
kak_assert(ranked_match_order_with_priority("", "Qualify as `std::collections::HashMap`", 1, "Extract type as type alias", 2));
456+
kak_assert(ranked_match_order_with_priority("as", "Qualify as `std::collections::HashMap`", 1, "Extract type as type alias", 2));
457+
438458
}};
439459

440460
UnitTest test_used_letters{[]()

src/ranked_match.hh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ using Priority = size_t;
3636

3737
struct RankedMatch
3838
{
39-
RankedMatch(StringView candidate, const RankedMatchQuery& query);
39+
RankedMatch(StringView candidate, const RankedMatchQuery& query, Optional<Priority> priority = {});
4040
RankedMatch(StringView candidate, UsedLetters candidate_letters,
41-
const RankedMatchQuery& query);
41+
const RankedMatchQuery& query, Optional<Priority> priority = {});
4242

4343
const StringView& candidate() const { return m_candidate; }
4444
bool operator<(const RankedMatch& other) const;
@@ -48,7 +48,7 @@ struct RankedMatch
4848

4949
private:
5050
template<typename TestFunc>
51-
RankedMatch(StringView candidate, const RankedMatchQuery& query, TestFunc test);
51+
RankedMatch(StringView candidate, const RankedMatchQuery& query, Optional<Priority> priority, TestFunc test);
5252

5353
StringView m_candidate{};
5454
bool m_matches = false;

0 commit comments

Comments
 (0)