Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow users to influence sorting of user-supplied completions #4813

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/pages/commands.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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*:::
Expand Down Expand Up @@ -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*:::
Expand Down Expand Up @@ -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 `<candidate>|<priority>`. In this case, any `|` or `\`
characters that occur within the `<candidate>` 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 <<options#types,`:doc options types`>>.

During the execution of the shell script, the following env vars are
available:

Expand Down
9 changes: 6 additions & 3 deletions doc/pages/options.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ are exclusively available to built-in options.
escaped as `\|` or `\\`.

*completions*::
a list of `<text>|<select cmd>|<menu text>` candidates,
a list of `<text>|<select cmd>|<menu text>[|<priority>]` candidates,
except for the first element which follows the
`<line>.<column>[+<length>]@<timestamp>` format to define where the
completion apply in the buffer.
Expand All @@ -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 `<line>.<column>` and the cursor) is a
subsequence of `<text>`.
query typed by the user (the text between `<line>.<column>` and the
cursor) is a subsequence of `<text>`. 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
`<priority>` field, which is a positive integer (lower is better).

For each remaining candidate, the completion menu displays
`<text>`, followed by `<menu text>`, which is a Markup string (see
Expand Down
11 changes: 11 additions & 0 deletions gdb/kakoune.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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<true>$', SubsequenceDistance)
return pp
4 changes: 2 additions & 2 deletions rc/tools/doc.kak
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ define-command -params 1 -hidden doc-render %{
map buffer normal <ret> :doc-follow-link<ret>
}

define-command doc -params 0..2 -menu -docstring %{
define-command doc -params 0..2 -docstring %{
doc <topic> [<keyword>]: 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

Expand Down Expand Up @@ -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 \
Expand Down
27 changes: 14 additions & 13 deletions src/array_view.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename T>
template<typename T, typename SizeType = std::size_t>
class ArrayView
{
public:
Expand All @@ -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)
Expand All @@ -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; }
Expand All @@ -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<typename T>
using ConstArrayView = ArrayView<const T>;
template<typename T, typename SizeType = std::size_t>
using ConstArrayView = ArrayView<const T, SizeType>;

template<typename T>
bool operator==(ArrayView<T> lhs, ArrayView<T> rhs)

template<typename T, typename SizeType>
bool operator==(ArrayView<T, SizeType> lhs, ArrayView<T, SizeType> rhs)
{
if (lhs.size() != rhs.size())
return false;
Expand All @@ -84,8 +85,8 @@ bool operator==(ArrayView<T> lhs, ArrayView<T> rhs)
return true;
}

template<typename T>
bool operator!=(ArrayView<T> lhs, ArrayView<T> rhs)
template<typename T, typename SizeType>
bool operator!=(ArrayView<T, SizeType> lhs, ArrayView<T, SizeType> rhs)
{
return not (lhs == rhs);
}
Expand Down
69 changes: 54 additions & 15 deletions src/commands.cc
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ static Completions complete_buffer_name(const Context& context, CompletionFlags
};

StringView query = prefix.substr(0, cursor_pos);
RankedMatchQuery q{query};
Vector<RankedMatchAndBuffer> filename_matches;
Vector<RankedMatchAndBuffer> matches;
for (const auto& buffer : BufferManager::instance())
Expand All @@ -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());
Expand Down Expand Up @@ -298,16 +299,34 @@ struct ShellCandidatesCompleter
m_candidates.clear();
for (auto c : output | split<StringView>('\n')
| filter([](auto s) { return not s.empty(); }))
m_candidates.emplace_back(c.str(), used_letters(c));
{
String candidate;
Optional<Priority> priority;
if (m_flags & Completions::Flags::Priority)
{
priority.emplace();
std::tie(candidate, *priority) = option_from_string(Meta::Type<std::tuple<String, Priority>>{}, 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<RankedMatch> 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);
}

Expand All @@ -327,7 +346,12 @@ struct ShellCandidatesCompleter

private:
String m_shell_script;
Vector<std::pair<String, UsedLetters>, MemoryDomain::Completion> m_candidates;
struct Candidate {
String candidate;
UsedLetters used_letters;
Optional<Priority> priority;
};
Vector<Candidate, MemoryDomain::Completion> m_candidates;
int m_token = -1;
Completions::Flags m_flags;
};
Expand Down Expand Up @@ -1204,6 +1228,8 @@ Vector<String> 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,
Expand Down Expand Up @@ -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"})
Expand All @@ -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];
Expand All @@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -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" } },
Expand Down Expand Up @@ -1429,14 +1468,15 @@ const CommandDesc complete_command_cmd = {
"complete-command [<switches>] <name> <type> [<param>]\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));
}
Expand Down Expand Up @@ -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" } },
Expand All @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/completion.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<Flags>) { return true; }
Expand Down Expand Up @@ -75,11 +76,11 @@ CandidateList complete(StringView query, ByteCount cursor_pos,
static_assert(not std::is_same<decltype(*begin(container)), String>::value,
"complete require long lived strings, not temporaries");

query = query.substr(0, cursor_pos);
RankedMatchQuery q{query.substr(0, cursor_pos)};
Vector<RankedMatch> 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());
Expand Down
Loading