Skip to content

Commit 7941886

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 647de2d commit 7941886

File tree

4 files changed

+79
-23
lines changed

4 files changed

+79
-23
lines changed

src/commands.cc

+49-14
Original file line numberDiff line numberDiff line change
@@ -278,16 +278,34 @@ struct ShellCandidatesCompleter
278278
m_candidates.clear();
279279
for (auto c : output | split<StringView>('\n')
280280
| filter([](auto s) { return not s.empty(); }))
281-
m_candidates.emplace_back(c.str(), used_letters(c));
281+
{
282+
String candidate;
283+
Optional<Priority> priority;
284+
if (m_flags & Completions::Flags::Priority)
285+
{
286+
priority.emplace();
287+
std::tie(candidate, *priority) = option_from_string(Meta::Type<std::tuple<String, Priority>>{}, c);
288+
if (m_flags & Completions::Flags::Priority and (int)*priority <= 0)
289+
{
290+
String error_message = "error computing shell-script-candidates: priority must be a positive integer";
291+
write_to_debug_buffer(error_message);
292+
throw runtime_error(std::move(error_message));
293+
}
294+
}
295+
else
296+
candidate = c.str();
297+
UsedLetters letters = used_letters(candidate);
298+
m_candidates.push_back(Candidate{std::move(candidate), letters, priority});
299+
}
282300
m_token = token_to_complete;
283301
}
284302

285303
StringView query = params[token_to_complete].substr(0, pos_in_token);
286304
RankedMatchQuery q{query, used_letters(query)};
287305
Vector<RankedMatch> matches;
288-
for (const auto& candidate : m_candidates)
306+
for (const auto& c : m_candidates)
289307
{
290-
if (RankedMatch match{candidate.first, candidate.second, q})
308+
if (RankedMatch match{c.candidate, c.used_letters, q, c.priority})
291309
matches.push_back(match);
292310
}
293311

@@ -307,7 +325,12 @@ struct ShellCandidatesCompleter
307325

308326
private:
309327
String m_shell_script;
310-
Vector<std::pair<String, UsedLetters>, MemoryDomain::Completion> m_candidates;
328+
struct Candidate {
329+
String candidate;
330+
UsedLetters used_letters;
331+
Optional<Priority> priority;
332+
};
333+
Vector<Candidate, MemoryDomain::Completion> m_candidates;
311334
int m_token = -1;
312335
Completions::Flags m_flags;
313336
};
@@ -1184,6 +1207,8 @@ Vector<String> params_to_shell(const ParametersParser& parser)
11841207

11851208
CommandCompleter make_command_completer(StringView type, StringView param, Completions::Flags completions_flags)
11861209
{
1210+
if (completions_flags & Completions::Flags::Priority and type != "shell-script-candidates")
1211+
throw runtime_error("-priority requires shell-script-candidates");
11871212
if (type == "file")
11881213
{
11891214
return [=](const Context& context, CompletionFlags flags,
@@ -1259,6 +1284,8 @@ CommandCompleter make_command_completer(StringView type, StringView param, Compl
12591284
}
12601285

12611286
static CommandCompleter parse_completion_switch(const ParametersParser& parser, Completions::Flags completions_flags) {
1287+
if (completions_flags & Completions::Flags::Priority and not parser.get_switch("shell-script-candidates"))
1288+
throw runtime_error("-priority requires -shell-script-candidates");
12621289
for (StringView completion_switch : {"file-completion", "client-completion", "buffer-completion",
12631290
"shell-script-completion", "shell-script-candidates",
12641291
"command-completion", "shell-completion"})
@@ -1274,6 +1301,16 @@ static CommandCompleter parse_completion_switch(const ParametersParser& parser,
12741301
return {};
12751302
}
12761303

1304+
static Completions::Flags make_completions_flags(const ParametersParser& parser)
1305+
{
1306+
Completions::Flags flags = Completions::Flags::None;
1307+
if (parser.get_switch("menu"))
1308+
flags |= Completions::Flags::Menu;
1309+
if (parser.get_switch("priority"))
1310+
flags |= Completions::Flags::Priority;
1311+
return flags;
1312+
}
1313+
12771314
void define_command(const ParametersParser& parser, Context& context, const ShellContext&)
12781315
{
12791316
const String& cmd_name = parser[0];
@@ -1289,10 +1326,6 @@ void define_command(const ParametersParser& parser, Context& context, const Shel
12891326
if (parser.get_switch("hidden"))
12901327
flags = CommandFlags::Hidden;
12911328

1292-
bool menu = (bool)parser.get_switch("menu");
1293-
const Completions::Flags completions_flags = menu ?
1294-
Completions::Flags::Menu : Completions::Flags::None;
1295-
12961329
const String& commands = parser[1];
12971330
CommandFunc cmd;
12981331
ParameterDesc desc;
@@ -1326,8 +1359,9 @@ void define_command(const ParametersParser& parser, Context& context, const Shel
13261359
};
13271360
}
13281361

1362+
const Completions::Flags completions_flags = make_completions_flags(parser);
13291363
CommandCompleter completer = parse_completion_switch(parser, completions_flags);
1330-
if (menu and not completer)
1364+
if (completions_flags & Completions::Flags::Menu and not completer)
13311365
throw runtime_error(format("menu switch requires a completion switch", cmd_name));
13321366
auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{}));
13331367

@@ -1345,6 +1379,7 @@ const CommandDesc define_command_cmd = {
13451379
{ "hidden", { false, "do not display the command in completion candidates" } },
13461380
{ "docstring", { true, "define the documentation string for command" } },
13471381
{ "menu", { false, "treat completions as the only valid inputs" } },
1382+
{ "priority", { false, "shell script candidates have candidate|priority syntax" } },
13481383
{ "file-completion", { false, "complete parameters using filename completion" } },
13491384
{ "client-completion", { false, "complete parameters using client name completion" } },
13501385
{ "buffer-completion", { false, "complete parameters using buffer name completion" } },
@@ -1412,14 +1447,15 @@ const CommandDesc complete_command_cmd = {
14121447
"complete-command [<switches>] <name> <type> [<param>]\n"
14131448
"define command completion",
14141449
ParameterDesc{
1415-
{ { "menu", { false, "treat completions as the only valid inputs" } }, },
1450+
{ { "menu", { false, "treat completions as the only valid inputs" } },
1451+
{ "priority", { false, "shell script candidates have candidate|priority syntax" } }, },
14161452
ParameterDesc::Flags::None, 2, 3},
14171453
CommandFlags::None,
14181454
CommandHelper{},
14191455
make_completer(complete_command_name),
14201456
[](const ParametersParser& parser, Context& context, const ShellContext&)
14211457
{
1422-
const Completions::Flags flags = parser.get_switch("menu") ? Completions::Flags::Menu : Completions::Flags::None;
1458+
const Completions::Flags flags = make_completions_flags(parser);
14231459
CommandCompleter completer = make_command_completer(parser[1], parser.positional_count() >= 3 ? parser[2] : StringView{}, flags);
14241460
CommandManager::instance().set_command_completer(parser[0], std::move(completer));
14251461
}
@@ -2158,6 +2194,7 @@ const CommandDesc prompt_cmd = {
21582194
{ { "init", { true, "set initial prompt content" } },
21592195
{ "password", { false, "Do not display entered text and clear reg after command" } },
21602196
{ "menu", { false, "treat completions as the only valid inputs" } },
2197+
{ "priority", { false, "shell script candidates have candidate|priority syntax" } },
21612198
{ "file-completion", { false, "use file completion for prompt" } },
21622199
{ "client-completion", { false, "use client completion for prompt" } },
21632200
{ "buffer-completion", { false, "use buffer completion for prompt" } },
@@ -2177,9 +2214,7 @@ const CommandDesc prompt_cmd = {
21772214
const String& command = parser[1];
21782215
auto initstr = parser.get_switch("init").value_or(StringView{});
21792216

2180-
const Completions::Flags completions_flags = parser.get_switch("menu") ?
2181-
Completions::Flags::Menu : Completions::Flags::None;
2182-
PromptCompleterAdapter completer = parse_completion_switch(parser, completions_flags);
2217+
PromptCompleterAdapter completer = parse_completion_switch(parser, make_completions_flags(parser));
21832218

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

src/completion.hh

+2-1
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

+25-5
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

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ using Priority = size_t;
3434

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

4141
const StringView& candidate() const { return m_candidate; }
4242
bool operator<(const RankedMatch& other) const;
@@ -46,7 +46,7 @@ struct RankedMatch
4646

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

5151
StringView m_candidate{};
5252
bool m_matches = false;

0 commit comments

Comments
 (0)