Skip to content

Commit 15cdb2b

Browse files
committed
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 be8eaa8 commit 15cdb2b

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
};
@@ -1205,6 +1228,8 @@ Vector<String> params_to_shell(const ParametersParser& parser)
12051228

12061229
CommandCompleter make_command_completer(StringView type, StringView param, Completions::Flags completions_flags)
12071230
{
1231+
if (completions_flags & Completions::Flags::Priority and type != "shell-script-candidates")
1232+
throw runtime_error("-priority requires shell-script-candidates");
12081233
if (type == "file")
12091234
{
12101235
return [=](const Context& context, CompletionFlags flags,
@@ -1280,6 +1305,8 @@ CommandCompleter make_command_completer(StringView type, StringView param, Compl
12801305
}
12811306

12821307
static CommandCompleter parse_completion_switch(const ParametersParser& parser, Completions::Flags completions_flags) {
1308+
if (completions_flags & Completions::Flags::Priority and not parser.get_switch("shell-script-candidates"))
1309+
throw runtime_error("-priority requires -shell-script-candidates");
12831310
for (StringView completion_switch : {"file-completion", "client-completion", "buffer-completion",
12841311
"shell-script-completion", "shell-script-candidates",
12851312
"command-completion", "shell-completion"})
@@ -1295,6 +1322,16 @@ static CommandCompleter parse_completion_switch(const ParametersParser& parser,
12951322
return {};
12961323
}
12971324

1325+
static Completions::Flags make_completions_flags(const ParametersParser& parser)
1326+
{
1327+
Completions::Flags flags = Completions::Flags::None;
1328+
if (parser.get_switch("menu"))
1329+
flags |= Completions::Flags::Menu;
1330+
if (parser.get_switch("priority"))
1331+
flags |= Completions::Flags::Priority;
1332+
return flags;
1333+
}
1334+
12981335
void define_command(const ParametersParser& parser, Context& context, const ShellContext&)
12991336
{
13001337
const String& cmd_name = parser[0];
@@ -1310,10 +1347,6 @@ void define_command(const ParametersParser& parser, Context& context, const Shel
13101347
if (parser.get_switch("hidden"))
13111348
flags = CommandFlags::Hidden;
13121349

1313-
bool menu = (bool)parser.get_switch("menu");
1314-
const Completions::Flags completions_flags = menu ?
1315-
Completions::Flags::Menu : Completions::Flags::None;
1316-
13171350
const String& commands = parser[1];
13181351
CommandFunc cmd;
13191352
ParameterDesc desc;
@@ -1347,8 +1380,9 @@ void define_command(const ParametersParser& parser, Context& context, const Shel
13471380
};
13481381
}
13491382

1383+
const Completions::Flags completions_flags = make_completions_flags(parser);
13501384
CommandCompleter completer = parse_completion_switch(parser, completions_flags);
1351-
if (menu and not completer)
1385+
if (completions_flags & Completions::Flags::Menu and not completer)
13521386
throw runtime_error(format("menu switch requires a completion switch", cmd_name));
13531387
auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{}));
13541388

@@ -1366,6 +1400,7 @@ const CommandDesc define_command_cmd = {
13661400
{ "hidden", { {}, "do not display the command in completion candidates" } },
13671401
{ "docstring", { ArgCompleter{}, "define the documentation string for command" } },
13681402
{ "menu", { {}, "treat completions as the only valid inputs" } },
1403+
{ "priority", { {}, "shell script candidates have candidate|priority syntax" } },
13691404
{ "file-completion", { {}, "complete parameters using filename completion" } },
13701405
{ "client-completion", { {}, "complete parameters using client name completion" } },
13711406
{ "buffer-completion", { {}, "complete parameters using buffer name completion" } },
@@ -1433,14 +1468,15 @@ const CommandDesc complete_command_cmd = {
14331468
"complete-command [<switches>] <name> <type> [<param>]\n"
14341469
"define command completion",
14351470
ParameterDesc{
1436-
{ { "menu", { {}, "treat completions as the only valid inputs" } }, },
1471+
{ { "menu", { {}, "treat completions as the only valid inputs" } },
1472+
{ "priority", { {}, "shell script candidates have candidate|priority syntax" } }, },
14371473
ParameterDesc::Flags::None, 2, 3},
14381474
CommandFlags::None,
14391475
CommandHelper{},
14401476
make_completer(complete_command_name),
14411477
[](const ParametersParser& parser, Context& context, const ShellContext&)
14421478
{
1443-
const Completions::Flags flags = parser.get_switch("menu") ? Completions::Flags::Menu : Completions::Flags::None;
1479+
const Completions::Flags flags = make_completions_flags(parser);
14441480
CommandCompleter completer = make_command_completer(parser[1], parser.positional_count() >= 3 ? parser[2] : StringView{}, flags);
14451481
CommandManager::instance().set_command_completer(parser[0], std::move(completer));
14461482
}
@@ -2179,6 +2215,7 @@ const CommandDesc prompt_cmd = {
21792215
{ { "init", { ArgCompleter{}, "set initial prompt content" } },
21802216
{ "password", { {}, "Do not display entered text and clear reg after command" } },
21812217
{ "menu", { {}, "treat completions as the only valid inputs" } },
2218+
{ "priority", { {}, "shell script candidates have candidate|priority syntax" } },
21822219
{ "file-completion", { {}, "use file completion for prompt" } },
21832220
{ "client-completion", { {}, "use client completion for prompt" } },
21842221
{ "buffer-completion", { {}, "use buffer completion for prompt" } },
@@ -2198,9 +2235,7 @@ const CommandDesc prompt_cmd = {
21982235
const String& command = parser[1];
21992236
auto initstr = parser.get_switch("init").value_or(StringView{});
22002237

2201-
const Completions::Flags completions_flags = parser.get_switch("menu") ?
2202-
Completions::Flags::Menu : Completions::Flags::None;
2203-
PromptCompleterAdapter completer = parse_completion_switch(parser, completions_flags);
2238+
PromptCompleterAdapter completer = parse_completion_switch(parser, make_completions_flags(parser));
22042239

22052240
const auto flags = parser.get_switch("password") ?
22062241
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)