xF: menu autocomplete

I'm not sure if this is simpler or more complex than listing candidates.

xC already has its old libedit hack, so I suppose I'm not going to
bother with it.
This commit is contained in:
2026-08-09 16:29:02 +02:00
parent 064102b786
commit 9f1ef95f46
+130 -38
View File
@@ -289,6 +289,7 @@ enum action
ACTION_BUFFER_LIST_SCROLL_BOTTOM,
ACTION_SUBMIT,
ACTION_COMPLETE,
ACTION_COMPLETE_BACKWARD,
ACTION_HISTORY_PREVIOUS,
ACTION_HISTORY_NEXT,
ACTION_HISTORY_FIRST,
@@ -499,10 +500,17 @@ static struct
struct poller_timer date_change_event;
struct line_editor editor; ///< Message editor
uint64_t editor_revision; ///< Changes with contents or caret
char *editor_running_for; ///< Buffer name being edited externally
char *editor_filename; ///< File being edited by the user
bool editor_shows_transcript; ///< External editor shows transcript
struct strv completions; ///< Stem followed by possible matches
size_t completions_at; ///< Currently selected completion
uint32_t completions_start; ///< Start of replacement range
uint32_t completions_end; ///< End of replacement range
uint64_t completions_revision; ///< Request/menu editor revision stamp
struct binding *keys; ///< Parsed xC-style key bindings
size_t keys_len;
@@ -872,6 +880,7 @@ app_on_date_change_timer (void *user_data)
static void
app_editor_changed (void)
{
g.editor_revision++;
xui_invalidate ();
}
@@ -922,18 +931,29 @@ app_editor_text (void)
static void
app_editor_set (const char *text)
{
line_editor_free (&g.editor);
app_editor_free ();
memset (&g.editor, 0, sizeof g.editor);
line_editor_start (&g.editor, 0);
app_editor_init ();
app_editor_changed ();
if (!text)
return;
const char *input = text ? text : "";
struct utf8_iter iter = { .s = input, .len = strlen (input) };
struct utf8_iter iter = utf8_iter_make (text);
int32_t codepoint = 0;
size_t codepoint_len = 0;
while ((codepoint = utf8_iter_next (&iter, &codepoint_len)) >= 0)
app_editor_insert (codepoint);
g.editor.on_changed = app_editor_changed;
xui_invalidate ();
}
static void
app_editor_set_at (const char *text, size_t position)
{
app_editor_set (text);
g.editor.point = 0;
struct utf8_iter iter = { .s = text, .len = position };
while (utf8_iter_next (&iter, NULL) >= 0)
g.editor.point++;
}
// --- Relay -------------------------------------------------------------------
@@ -986,6 +1006,81 @@ relay_send (struct relay_command_message *m, uint32_t *command_seq)
// ~~~ Completion request ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
static void
completion_menu_reset (void)
{
strv_free (&g.completions);
g.completions = (struct strv) {};
}
static size_t
completion_replace (const char *current,
size_t start, size_t end, const char *word, bool append_space)
{
struct str completed = str_make ();
str_append_data (&completed, current, start);
str_append (&completed, word);
if (append_space)
str_append_c (&completed, ' ');
size_t caret = completed.len;
str_append (&completed, current + end);
app_editor_set_at (completed.str, caret);
str_free (&completed);
return caret;
}
static void
completion_menu_apply (size_t word)
{
hard_assert (word < g.completions.len);
char *current = app_editor_text ();
g.completions_at = word;
g.completions_end = completion_replace (current,
g.completions_start, g.completions_end,
g.completions.vector[word], !!word);
g.completions_revision = g.editor_revision;
free (current);
}
static bool
completion_menu_cycle (bool backwards)
{
if (g.completions.len < 2 || g.completions_revision != g.editor_revision)
return false;
completion_menu_apply (backwards
? (g.completions_at ? g.completions_at - 1 : g.completions.len - 1)
: (g.completions_at + 1) % g.completions.len);
return true;
}
static bool
completion_apply_response (const char *current, uint32_t start, uint32_t end,
const struct str *completions, uint32_t completions_len)
{
completion_menu_reset ();
if (start > end || end > strlen (current) || !completions_len)
return false;
if (completions_len == 1)
{
completion_replace (current, start, end, completions[0].str, true);
return true;
}
g.completions_start = start;
g.completions_end = end;
g.completions = strv_make ();
for (uint32_t i = 0; i < completions_len; i++)
strv_append (&g.completions, completions[i].str);
completion_menu_apply (0);
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
completion_request_start (struct completion_request *self, uint32_t request_seq,
char *text_owned, const char *buffer_name, uint32_t position)
@@ -1001,11 +1096,9 @@ completion_request_start (struct completion_request *self, uint32_t request_seq,
static void
completion_request_reset (struct completion_request *self)
{
self->pending = false;
self->request_seq = 0;
self->position = 0;
cstr_set (&self->text, NULL);
cstr_set (&self->buffer_name, NULL);
free (self->text);
free (self->buffer_name);
memset (self, 0, sizeof *self);
}
static void
@@ -1013,33 +1106,17 @@ app_process_completion_response
(const struct relay_response_data_buffer_complete *response)
{
char *current = app_editor_text ();
bool current_request = g.buffer_current
&& g.completion.buffer_name
&& !strcmp (g.buffer_current->buffer_name, g.completion.buffer_name)
&& !strcmp (current, g.completion.text)
&& app_editor_position () == g.completion.position;
if (current_request
&& response->start <= g.completion.position
&& g.completion.position <= strlen (current))
if (g.buffer_current && g.completion.buffer_name
&& !strcmp (g.buffer_current->buffer_name, g.completion.buffer_name)
&& !strcmp (current, g.completion.text)
&& app_editor_position () == g.completion.position
&& g.completions_revision == g.editor_revision)
{
if (response->completions_len)
{
struct str completed = str_make ();
str_append_data (&completed, current, response->start);
str_append_str (&completed, &response->completions[0]);
if (response->completions_len == 1)
str_append_c (&completed, ' ');
str_append (&completed, current + g.completion.position);
app_editor_set (completed.str);
str_free (&completed);
}
if (response->completions_len != 1)
if (!completion_apply_response (current,
response->start, g.completion.position,
response->completions, response->completions_len))
g_xui.ui->beep ();
}
else if (current_request)
g_xui.ui->beep ();
free (current);
completion_request_reset (&g.completion);
}
@@ -1179,6 +1256,7 @@ app_reset_relay_state (void)
log_view_hide (&g.log);
app_editor_set ("");
completion_menu_reset ();
}
static void
@@ -3400,9 +3478,15 @@ app_switch_buffer_number (int number)
}
static bool
app_complete (void)
app_complete (bool backwards)
{
if (!g.buffer_current || g.completion.pending)
if (g.completion.pending)
return false;
if (completion_menu_cycle (backwards))
return true;
completion_menu_reset ();
if (!g.buffer_current)
return false;
char *text = app_editor_text ();
@@ -3429,6 +3513,7 @@ app_complete (void)
completion_request_start (&g.completion, request_seq, text,
g.buffer_current->buffer_name, position);
g.completions_revision = g.editor_revision;
return true;
}
@@ -3791,7 +3876,11 @@ app_process_editor_action (enum action action)
enum line_editor_action editor_action = g_editor_actions[index];
bool handled = line_editor_action (&g.editor, editor_action);
if (handled)
{
// Motions do not invoke the editor's change callback.
g.editor_revision++;
xui_invalidate ();
}
return handled;
}
@@ -3890,7 +3979,9 @@ app_process_action (enum action action)
app_submit_input ();
return true;
case ACTION_COMPLETE:
return app_complete ();
return app_complete (false);
case ACTION_COMPLETE_BACKWARD:
return app_complete (true);
case ACTION_HISTORY_PREVIOUS:
return app_history (-1);
case ACTION_HISTORY_NEXT:
@@ -4045,6 +4136,7 @@ g_default_bindings[] =
{ "Enter", ACTION_SUBMIT },
{ "C-j", ACTION_SUBMIT },
{ "Tab", ACTION_COMPLETE }, // Readline
{ "S-Tab", ACTION_COMPLETE_BACKWARD },
{ "M-p", ACTION_HISTORY_PREVIOUS }, // xP/xC
{ "Up", ACTION_HISTORY_PREVIOUS }, // Readline
{ "M-n", ACTION_HISTORY_NEXT }, // xP/xC