Files
cproof-context/playbooks/add-autocomplete.md
jabber.developer2 22977846a3 docs: split context into layered, agent-oriented files
Replace the single file-structure.md with a stratified layout designed
for AI/agent skill consumption: tables and concrete identifiers over
prose, files loaded on demand, content separated by churn rate.

Layers:
- architecture/  stable structural reference (overview, source-map,
                 test-map, data-flow)
- patterns/      memory, commands, autocomplete, events, xmpp,
                 encryption, ui, plugins
- testing/       unit-tests, stubs, functional-tests, bench
- build/         local, docker, ci
- playbooks/     add-command, add-test, add-autocomplete,
                 add-event-handler, add-encryption
- gotchas.md     append-only dated entries (seven seed entries)
- wip/           branch-specific notes; deleted on merge to master

Stable layers describe cproof on master only. In-flight feature
branches (currently feat/ai) get a single file under wip/.

INDEX.md is the entry map with churn labels; SKILL.md is the
always-loaded skill hint pointing to it.
2026-04-30 20:52:06 +03:00

3.3 KiB

Playbook: add autocomplete to a command

Pre-req: command already exists in cmd_defs.c. If not, see playbooks/add-command.md.

Choose the flavour

Token set Use
Static, known at startup. autocomplete_param_with_ac + a static Autocomplete.
Dynamic — depends on roster, accounts, DB query, etc. autocomplete_param_with_func + a stateless callback.

Static token set

src/command/cmd_ac.c:

// 1. Declare the AC at file scope:
static Autocomplete foo_ac;

// 2. Add it to the static array near the top of the file (the free-list
//    that cmd_ac_uninit walks at shutdown):
static Autocomplete* all_acs[] = {
    // ... existing entries ...
    &foo_ac,
};

// 3. Initialise in cmd_ac_init():
foo_ac = autocomplete_new();
autocomplete_add(foo_ac, "alpha");
autocomplete_add(foo_ac, "beta");

// 4. Implement the dispatcher:
static char*
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
    return autocomplete_param_with_ac(input, "/foo", foo_ac, TRUE, previous);
}

// 5. Register:
g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete);

Dynamic — function callback

If suggestions come from runtime state (roster, accounts, providers list):

// In a domain module (e.g. src/foo/foo.c), implement a stateless callback:
char*
foo_suggestions(const char* const search_str, gboolean previous, void* context)
{
    // Compute and return a freshly-allocated char* (caller frees), or NULL.
    // No module-level "_last_match" globals — keep state on the stack /
    // derive deterministically from search_str and previous.
}

Wire into cmd_ac.c:

static char*
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
    return autocomplete_param_with_func(input, "/foo", foo_suggestions, previous, NULL);
}

g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete);

A clean canonical example is roster_contact_autocomplete in src/xmpp/roster_list.c — it delegates to autocomplete_complete against a roster-owned Autocomplete and keeps no callback-local state. Do not copy older callbacks that keep state in a file-static _last_match variable — those have known issues with shift-tab cycling and concurrent completers.

Subcommand autocompletion

If /foo has subcommands, branch inside _foo_autocomplete:

static char*
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
    char* result = NULL;

    // First the subcommand list itself:
    result = autocomplete_param_with_ac(input, "/foo", foo_subcommands_ac, TRUE, previous);
    if (result) return result;

    // Then per-subcommand argument completion:
    if (g_str_has_prefix(input, "/foo set ")) {
        result = autocomplete_param_with_func(input, "/foo set", foo_set_options, previous, NULL);
        if (result) return result;
    }

    return NULL;
}

/roster (src/command/cmd_ac.c, search for _roster_autocomplete) is a current example of this layered subcommand layout.

Unit-test the callback

Stateless callbacks are easy to unit-test directly:

void
test_foo_suggestions_returns_first_match(void** state)
{
    char* r = foo_suggestions("al", FALSE, NULL);
    assert_string_equal(r, "alpha");
    free(r);
}

Add the test pair to tests/unittests/ (see playbooks/add-test.md).