Make shell-script-candidates completer run in the background

Read output from the script as it comes and update the candidate
list progressively.

Disable updating of the list when a completion has been explicitely
selected.
This commit is contained in:
Maxime Coste 2023-11-07 18:01:54 +11:00
parent 719512b308
commit 11f0ace9b6
14 changed files with 171 additions and 62 deletions

View File

@ -5,6 +5,9 @@ released versions.
== Development version == Development version
* `shell-script-candidates` completion now runs the script asynchronously
while displaying and updating results live.
* `%val{window_range}` elements are now emitted as different strings * `%val{window_range}` elements are now emitted as different strings
* `+` only duplicates identical selections a single time to avoid surprising * `+` only duplicates identical selections a single time to avoid surprising

View File

@ -280,53 +280,85 @@ struct ShellCandidatesCompleter
Completions::Flags flags = Completions::Flags::None) Completions::Flags flags = Completions::Flags::None)
: m_shell_script{std::move(shell_script)}, m_flags(flags) {} : m_shell_script{std::move(shell_script)}, m_flags(flags) {}
ShellCandidatesCompleter(const ShellCandidatesCompleter& other) : m_shell_script{other.m_shell_script}, m_flags(other.m_flags) {}
ShellCandidatesCompleter& operator=(const ShellCandidatesCompleter& other) { m_shell_script = other.m_shell_script; m_flags = other.m_flags; return *this; }
Completions operator()(const Context& context, CompletionFlags flags, Completions operator()(const Context& context, CompletionFlags flags,
CommandParameters params, size_t token_to_complete, CommandParameters params, size_t token_to_complete,
ByteCount pos_in_token) ByteCount pos_in_token)
{ {
if (m_token != token_to_complete) if (m_last_token != token_to_complete)
{ {
ShellContext shell_context{ ShellContext shell_context{
params, params,
{ { "token_to_complete", to_string(token_to_complete) } } { { "token_to_complete", to_string(token_to_complete) } }
}; };
String output = ShellManager::instance().eval(m_shell_script, context, {}, m_running_script.emplace(ShellManager::instance().spawn(m_shell_script, context, false, shell_context));
ShellManager::Flags::WaitForStdout, m_watcher.emplace((int)m_running_script->out, FdEvents::Read, EventMode::Urgent,
shell_context).first; [this, &input_handler=context.input_handler()](auto&&... args) { read_candidates(input_handler); });
m_candidates.clear(); m_candidates.clear();
for (auto c : output | split<StringView>('\n') m_last_token = token_to_complete;
| filter([](auto s) { return not s.empty(); })) }
m_candidates.emplace_back(c.str(), used_letters(c)); return rank_candidates(params[token_to_complete].substr(0, pos_in_token));
m_token = token_to_complete;
} }
StringView query = params[token_to_complete].substr(0, pos_in_token); private:
UsedLetters query_letters = used_letters(query); void read_candidates(InputHandler& input_handler)
Vector<RankedMatch> matches;
for (const auto& candidate : m_candidates)
{ {
if (RankedMatch match{candidate.first, candidate.second, query, query_letters}) char buffer[2048];
matches.push_back(match); bool closed = false;
int fd = (int)m_running_script->out;
while (fd_readable(fd))
{
int size = read(fd, buffer, sizeof(buffer));
if (size == 0)
{
closed = true;
break;
} }
m_stdout_buffer.insert(m_stdout_buffer.end(), buffer, buffer + size);
}
auto end = closed ? m_stdout_buffer.end() : find(m_stdout_buffer | reverse(), '\n').base();
for (auto c : ArrayView(m_stdout_buffer.begin(), end) | split<StringView>('\n')
| filter([](auto s) { return not s.empty(); }))
m_candidates.emplace_back(c.str(), used_letters(c));
m_stdout_buffer.erase(m_stdout_buffer.begin(), end);
input_handler.refresh_ifn();
if (not closed)
return;
m_running_script.reset();
m_watcher.reset();
}
Completions rank_candidates(StringView query)
{
UsedLetters query_letters = used_letters(query);
auto matches = m_candidates | transform([&](const auto& c) { return RankedMatch{c.first, c.second, query, query_letters}; })
| filter([](const auto& m) { return (bool)m; })
| gather<Vector<RankedMatch>>();
constexpr size_t max_count = 100; constexpr size_t max_count = 100;
CandidateList res; CandidateList res;
// Gather best max_count matches // Gather best max_count matches
for_n_best(matches, max_count, [](auto& lhs, auto& rhs) { return rhs < lhs; }, for_n_best(matches, max_count, [](auto& lhs, auto& rhs) { return rhs < lhs; }, [&] (const RankedMatch& m) {
[&] (const RankedMatch& m) {
if (not res.empty() and res.back() == m.candidate()) if (not res.empty() and res.back() == m.candidate())
return false; return false;
res.push_back(m.candidate().str()); res.push_back(m.candidate().str());
return true; return true;
}); });
return Completions{0_byte, pos_in_token, std::move(res), m_flags}; return Completions{0_byte, query.length(), std::move(res), m_flags};
} }
private:
String m_shell_script; String m_shell_script;
Vector<char, MemoryDomain::Completion> m_stdout_buffer;
Optional<Shell> m_running_script;
Optional<FDWatcher> m_watcher;
Vector<std::pair<String, UsedLetters>, MemoryDomain::Completion> m_candidates; Vector<std::pair<String, UsedLetters>, MemoryDomain::Completion> m_candidates;
int m_token = -1; int m_last_token = -1;
Completions::Flags m_flags; Completions::Flags m_flags;
}; };

View File

@ -37,6 +37,8 @@ public:
virtual void on_enabled(bool from_pop) {} virtual void on_enabled(bool from_pop) {}
virtual void on_disabled(bool from_push) {} virtual void on_disabled(bool from_push) {}
virtual void refresh_ifn() {}
bool enabled() const { return &m_input_handler.current_mode() == this; } bool enabled() const { return &m_input_handler.current_mode() == this; }
Context& context() const { return m_input_handler.context(); } Context& context() const { return m_input_handler.context(); }
@ -1048,6 +1050,19 @@ public:
m_idle_timer.set_next_date(Clock::now() + get_idle_timeout(context())); m_idle_timer.set_next_date(Clock::now() + get_idle_timeout(context()));
} }
void refresh_ifn()
{
bool explicit_completion_selected = m_current_completion != -1 and
(not m_prefix_in_completions or m_current_completion != m_completions.candidates.size() - 1);
if (not enabled() or (context().flags() & Context::Flags::Draft) or explicit_completion_selected)
return;
if (auto next_date = Clock::now() + get_idle_timeout(context());
next_date < m_idle_timer.next_date())
m_idle_timer.set_next_date(next_date);
m_refresh_completion_pending = true;
}
void paste(StringView content) override void paste(StringView content) override
{ {
m_line_editor.insert(content); m_line_editor.insert(content);
@ -1122,14 +1137,12 @@ private:
if (menu) if (menu)
context().client().menu_select(0); context().client().menu_select(0);
auto prefix = line.substr(m_completions.start, m_completions.end - m_completions.start); auto prefix = line.substr(m_completions.start, m_completions.end - m_completions.start);
if (not menu and not contains(m_completions.candidates, prefix)) m_prefix_in_completions = not menu and not contains(m_completions.candidates, prefix);
if (m_prefix_in_completions)
{ {
m_current_completion = m_completions.candidates.size(); m_current_completion = m_completions.candidates.size();
m_completions.candidates.push_back(prefix.str()); m_completions.candidates.push_back(prefix.str());
m_prefix_in_completions = true;
} }
else
m_prefix_in_completions = false;
} catch (runtime_error&) {} } catch (runtime_error&) {}
} }
@ -1814,6 +1827,11 @@ void InputHandler::handle_key(Key key)
} }
} }
void InputHandler::refresh_ifn()
{
current_mode().refresh_ifn();
}
void InputHandler::start_recording(char reg) void InputHandler::start_recording(char reg)
{ {
kak_assert(m_recording_reg == 0); kak_assert(m_recording_reg == 0);

View File

@ -99,6 +99,8 @@ public:
// process the given key // process the given key
void handle_key(Key key); void handle_key(Key key);
void refresh_ifn();
void start_recording(char reg); void start_recording(char reg);
bool is_recording() const; bool is_recording() const;
void stop_recording(); void stop_recording();

View File

@ -46,6 +46,7 @@ struct {
StringView notes; StringView notes;
} constexpr version_notes[] = { { } constexpr version_notes[] = { {
0, 0,
"» asynchronous {+u}shell-script-candidates{} completion\n"
"» {+b}%val{window_range}{} is now emitted as separate strings\n" "» {+b}%val{window_range}{} is now emitted as separate strings\n"
"» {+b}+{} only duplicates identical selections a single time\n" "» {+b}+{} only duplicates identical selections a single time\n"
"» {+u}daemonize-session{} command\n" "» {+u}daemonize-session{} command\n"

View File

@ -86,26 +86,6 @@ ShellManager::ShellManager(ConstArrayView<EnvVarDesc> builtin_env_vars)
namespace namespace
{ {
struct UniqueFd
{
UniqueFd(int fd = -1) : fd{fd} {}
UniqueFd(UniqueFd&& other) : fd{other.fd} { other.fd = -1; }
UniqueFd& operator=(UniqueFd&& other) { std::swap(fd, other.fd); other.close(); return *this; }
~UniqueFd() { close(); }
explicit operator bool() const { return fd != -1; }
void close() { if (fd != -1) { ::close(fd); fd = -1; } }
int fd;
};
struct Shell
{
pid_t pid;
UniqueFd in;
UniqueFd out;
UniqueFd err;
};
Shell spawn_shell(const char* shell, StringView cmdline, Shell spawn_shell(const char* shell, StringView cmdline,
ConstArrayView<String> params, ConstArrayView<String> params,
ConstArrayView<String> kak_env, ConstArrayView<String> kak_env,
@ -145,13 +125,13 @@ Shell spawn_shell(const char* shell, StringView cmdline,
close(oldfd); close(oldfd);
}; };
renamefd(stdin_pipe[0].fd, 0); renamefd((int)stdin_pipe[0], 0);
renamefd(stdout_pipe[1].fd, 1); renamefd((int)stdout_pipe[1], 1);
renamefd(stderr_pipe[1].fd, 2); renamefd((int)stderr_pipe[1], 2);
close(stdin_pipe[1].fd); close((int)stdin_pipe[1]);
close(stdout_pipe[0].fd); close((int)stdout_pipe[0]);
close(stderr_pipe[0].fd); close((int)stderr_pipe[0]);
execve(shell, (char* const*)execparams.data(), (char* const*)envptrs.data()); execve(shell, (char* const*)execparams.data(), (char* const*)envptrs.data());
char buffer[1024]; char buffer[1024];
@ -215,13 +195,13 @@ FDWatcher make_reader(int fd, String& contents, OnClose&& on_close)
FDWatcher make_pipe_writer(UniqueFd& fd, StringView contents) FDWatcher make_pipe_writer(UniqueFd& fd, StringView contents)
{ {
int flags = fcntl(fd.fd, F_GETFL, 0); int flags = fcntl((int)fd, F_GETFL, 0);
fcntl(fd.fd, F_SETFL, flags | O_NONBLOCK); fcntl((int)fd, F_SETFL, flags | O_NONBLOCK);
return {fd.fd, FdEvents::Write, EventMode::Urgent, return {(int)fd, FdEvents::Write, EventMode::Urgent,
[contents, &fd](FDWatcher& watcher, FdEvents, EventMode) mutable { [contents, &fd](FDWatcher& watcher, FdEvents, EventMode) mutable {
while (fd_writable(fd.fd)) while (fd_writable((int)fd))
{ {
ssize_t size = ::write(fd.fd, contents.begin(), ssize_t size = ::write((int)fd, contents.begin(),
(size_t)contents.length()); (size_t)contents.length());
if (size > 0) if (size > 0)
contents = contents.substr(ByteCount{(int)size}); contents = contents.substr(ByteCount{(int)size});
@ -310,8 +290,8 @@ std::pair<String, int> ShellManager::eval(
auto wait_time = Clock::now(); auto wait_time = Clock::now();
String stdout_contents, stderr_contents; String stdout_contents, stderr_contents;
auto stdout_reader = make_reader(shell.out.fd, stdout_contents, [&](bool){ shell.out.close(); }); auto stdout_reader = make_reader((int)shell.out, stdout_contents, [&](bool){ shell.out.close(); });
auto stderr_reader = make_reader(shell.err.fd, stderr_contents, [&](bool){ shell.err.close(); }); auto stderr_reader = make_reader((int)shell.err, stderr_contents, [&](bool){ shell.err.close(); });
auto stdin_writer = make_pipe_writer(shell.in, input); auto stdin_writer = make_pipe_writer(shell.in, input);
// block SIGCHLD to make sure we wont receive it before // block SIGCHLD to make sure we wont receive it before
@ -324,7 +304,7 @@ std::pair<String, int> ShellManager::eval(
int status = 0; int status = 0;
// check for termination now that SIGCHLD is blocked // check for termination now that SIGCHLD is blocked
bool terminated = waitpid(shell.pid, &status, WNOHANG) != 0; bool terminated = waitpid((int)shell.pid, &status, WNOHANG) != 0;
bool failed = false; bool failed = false;
using namespace std::chrono; using namespace std::chrono;
@ -357,7 +337,7 @@ std::pair<String, int> ShellManager::eval(
} }
catch (cancel&) catch (cancel&)
{ {
kill(shell.pid, SIGINT); kill((int)shell.pid, SIGINT);
cancelling = true; cancelling = true;
} }
catch (runtime_error& error) catch (runtime_error& error)
@ -366,7 +346,7 @@ std::pair<String, int> ShellManager::eval(
failed = true; failed = true;
} }
if (not terminated) if (not terminated)
terminated = waitpid(shell.pid, &status, WNOHANG) == shell.pid; terminated = waitpid((int)shell.pid, &status, WNOHANG) == (int)shell.pid;
} }
if (not stderr_contents.empty()) if (not stderr_contents.empty())
@ -394,6 +374,18 @@ std::pair<String, int> ShellManager::eval(
return { std::move(stdout_contents), WIFEXITED(status) ? WEXITSTATUS(status) : -1 }; return { std::move(stdout_contents), WIFEXITED(status) ? WEXITSTATUS(status) : -1 };
} }
Shell ShellManager::spawn(StringView cmdline, const Context& context,
bool open_stdin, const ShellContext& shell_context)
{
auto kak_env = generate_env(cmdline, context, [&](StringView name, Quoting quoting) {
if (auto it = shell_context.env_vars.find(name); it != shell_context.env_vars.end())
return it->value;
return join(get_val(name, context) | transform(quoter(quoting)), ' ', false);
});
return spawn_shell(m_shell.c_str(), cmdline, shell_context.params, kak_env, open_stdin);
}
Vector<String> ShellManager::get_val(StringView name, const Context& context) const Vector<String> ShellManager::get_val(StringView name, const Context& context) const
{ {
auto env_var = find_if(m_env_vars, [name](const EnvVarDesc& desc) { auto env_var = find_if(m_env_vars, [name](const EnvVarDesc& desc) {

View File

@ -5,8 +5,13 @@
#include "env_vars.hh" #include "env_vars.hh"
#include "string.hh" #include "string.hh"
#include "utils.hh" #include "utils.hh"
#include "unique_descriptor.hh"
#include "completion.hh" #include "completion.hh"
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
namespace Kakoune namespace Kakoune
{ {
@ -27,6 +32,19 @@ struct EnvVarDesc
Retriever func; Retriever func;
}; };
inline void closepid(int pid){ kill(pid, SIGTERM); int status = 0; waitpid(pid, &status, 0); }
using UniqueFd = UniqueDescriptor<::close>;
using UniquePid = UniqueDescriptor<closepid>;
struct Shell
{
UniquePid pid;
UniqueFd in;
UniqueFd out;
UniqueFd err;
};
class ShellManager : public Singleton<ShellManager> class ShellManager : public Singleton<ShellManager>
{ {
public: public:
@ -44,6 +62,11 @@ public:
Flags flags = Flags::WaitForStdout, Flags flags = Flags::WaitForStdout,
const ShellContext& shell_context = {}); const ShellContext& shell_context = {});
Shell spawn(StringView cmdline,
const Context& context,
bool open_stdin,
const ShellContext& shell_complete = {});
Vector<String> get_val(StringView name, const Context& context) const; Vector<String> get_val(StringView name, const Context& context) const;
CandidateList complete_env_var(StringView prefix, ByteCount cursor_pos) const; CandidateList complete_env_var(StringView prefix, ByteCount cursor_pos) const;

23
src/unique_descriptor.hh Normal file
View File

@ -0,0 +1,23 @@
#ifndef fd_hh_INCLUDED
#define fd_hh_INCLUDED
namespace Kakoune
{
template<auto close_fn>
struct UniqueDescriptor
{
UniqueDescriptor(int descriptor = -1) : descriptor{descriptor} {}
UniqueDescriptor(UniqueDescriptor&& other) : descriptor{other.descriptor} { other.descriptor = -1; }
UniqueDescriptor& operator=(UniqueDescriptor&& other) { std::swap(descriptor, other.descriptor); other.close(); return *this; }
~UniqueDescriptor() { close(); }
explicit operator int() const { return descriptor; }
explicit operator bool() const { return descriptor != -1; }
void close() { if (descriptor != -1) { close_fn(descriptor); descriptor = -1; } }
int descriptor;
};
}
#endif // fd_hh_INCLUDED

View File

@ -1 +1 @@
:my-command <s-tab><s-tab><ret>

View File

@ -1 +1,2 @@
set global autocomplete prompt
def my-command -params 0..1 -shell-script-candidates %{ printf "aaa\nbbb\nccc" } %{ exec i %arg{1} <esc> } def my-command -params 0..1 -shell-script-candidates %{ printf "aaa\nbbb\nccc" } %{ exec i %arg{1} <esc> }

View File

@ -0,0 +1,6 @@
ui_out -ignore 7
ui_in '{ "jsonrpc": "2.0", "method": "keys", "params": [ ":my-command " ] }'
ui_out -ignore 7
ui_out '{ "jsonrpc": "2.0", "method": "refresh", "params": [false] }'
ui_out '{ "jsonrpc": "2.0", "method": "menu_show", "params": [[[{ "face": { "fg": "default", "bg": "default", "underline": "default", "attributes": [] }, "contents": "aaa" }], [{ "face": { "fg": "default", "bg": "default", "underline": "default", "attributes": [] }, "contents": "bbb" }], [{ "face": { "fg": "default", "bg": "default", "underline": "default", "attributes": [] }, "contents": "ccc" }]], { "line": 0, "column": 0 }, { "fg": "white", "bg": "blue", "underline": "default", "attributes": [] }, { "fg": "blue", "bg": "white", "underline": "default", "attributes": [] }, "prompt"] }'
ui_in '{ "jsonrpc": "2.0", "method": "keys", "params": [ "<s-tab><ret>" ] }'

View File

@ -1 +1 @@
:foo<ret>b<tab><tab><ret>

View File

@ -1,3 +1,4 @@
set-option global autocomplete prompt
define-command foo %{ define-command foo %{
prompt -shell-script-candidates %{ printf 'foo\nbar\nhaz\n' } ': ' %{exec i %val{text} <esc>} prompt -shell-script-candidates %{ printf 'foo\nbar\nhaz\n' } ': ' %{exec i %val{text} <esc>}
} }

View File

@ -0,0 +1,7 @@
ui_out -ignore 7
ui_in '{ "jsonrpc": "2.0", "method": "keys", "params": [ ":foo<ret>b" ] }'
ui_out -ignore 4
ui_out '{ "jsonrpc": "2.0", "method": "refresh", "params": [false] }'
ui_out -ignore 3
ui_out '{ "jsonrpc": "2.0", "method": "menu_show", "params": [[[{ "face": { "fg": "default", "bg": "default", "underline": "default", "attributes": [] }, "contents": "bar" }]], { "line": 0, "column": 0 }, { "fg": "white", "bg": "blue", "underline": "default", "attributes": [] }, { "fg": "blue", "bg": "white", "underline": "default", "attributes": [] }, "prompt"] }'
ui_in '{ "jsonrpc": "2.0", "method": "keys", "params": [ "<tab><ret>" ] }'