refactor(ai): use stateful autocomplete for provider name matching

Replace the manual GHashTable iteration in ai_providers_find() with the
existing stateful autocomplete system. Provider names are now kept in
sync via autocomplete_add/remove during ai_add_provider/ai_remove_provider,
and ai_providers_find delegates to autocomplete_complete() for deterministic
tab-completion cycling through sorted results.

This eliminates manual GList allocation, iteration, and cleanup, while
providing consistent forward/backward cycling behavior shared by all
autocomplete callers.
This commit is contained in:
2026-04-30 23:16:12 +00:00
parent 002a6ed15b
commit 461c0c32dd

View File

@@ -15,6 +15,7 @@
#include "log.h" #include "log.h"
#include "config/preferences.h" #include "config/preferences.h"
#include "profanity.h" #include "profanity.h"
#include "tools/autocomplete.h"
#include "ui/ui.h" #include "ui/ui.h"
#include <curl/curl.h> #include <curl/curl.h>
@@ -28,6 +29,7 @@
/* Global state */ /* Global state */
static GHashTable* providers = NULL; static GHashTable* providers = NULL;
static GHashTable* provider_keys = NULL; static GHashTable* provider_keys = NULL;
static Autocomplete providers_ac = NULL;
/* ======================================================================== /* ========================================================================
* Curl helpers * Curl helpers
@@ -245,6 +247,9 @@ ai_client_init(void)
providers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)ai_provider_unref); providers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify)ai_provider_unref);
provider_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); provider_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
/* Create autocomplete for provider names */
providers_ac = autocomplete_new();
/* Add default providers */ /* Add default providers */
ai_add_provider("openai", DEFAULT_OPENAI_URL, NULL); ai_add_provider("openai", DEFAULT_OPENAI_URL, NULL);
ai_add_provider("perplexity", DEFAULT_PERPLEXITY_URL, NULL); ai_add_provider("perplexity", DEFAULT_PERPLEXITY_URL, NULL);
@@ -266,6 +271,11 @@ ai_client_shutdown(void)
providers = NULL; providers = NULL;
provider_keys = NULL; provider_keys = NULL;
if (providers_ac) {
autocomplete_free(providers_ac);
providers_ac = NULL;
}
curl_global_cleanup(); curl_global_cleanup();
log_info("AI client shutdown"); log_info("AI client shutdown");
} }
@@ -304,6 +314,9 @@ ai_add_provider(const gchar* name, const gchar* api_url, const gchar* org_id)
AIProvider* provider = ai_provider_new(name, api_url, org_id); AIProvider* provider = ai_provider_new(name, api_url, org_id);
g_hash_table_insert(providers, g_strdup(name), provider); g_hash_table_insert(providers, g_strdup(name), provider);
/* Sync autocomplete */
autocomplete_add(providers_ac, name);
log_info("Added provider: %s (URL: %s)", name, api_url); log_info("Added provider: %s (URL: %s)", name, api_url);
return provider; /* Caller gets non-owning pointer; hash table owns ref */ return provider; /* Caller gets non-owning pointer; hash table owns ref */
} }
@@ -313,6 +326,10 @@ ai_remove_provider(const gchar* name)
{ {
if (!name || !providers) if (!name || !providers)
return FALSE; return FALSE;
/* Sync autocomplete before removing */
autocomplete_remove(providers_ac, name);
return g_hash_table_remove(providers, name); return g_hash_table_remove(providers, name);
} }
@@ -334,40 +351,23 @@ ai_list_providers(void)
return result; return result;
} }
/* ========================================================================
* Provider autocomplete state
* ======================================================================== */
/* Stateful provider name finder with case-sensitive matching.
* Maintains position in sorted list for deterministic tab-completion cycling.
* The autocomplete is kept in sync via ai_add_provider/ai_remove_provider. */
char* char*
ai_providers_find(const char* const search_str, gboolean previous, void* context) ai_providers_find(const char* const search_str, gboolean previous, void* context)
{ {
if (!providers || !search_str || strlen(search_str) == 0) { /* Initialize autocomplete on first use */
return NULL; if (!providers_ac) {
providers_ac = autocomplete_new();
} }
GHashTableIter iter; /* Use stateful autocomplete */
gpointer key; return autocomplete_complete(providers_ac, search_str, FALSE, previous);
gpointer value;
/* Collect all matching providers */
GList* matches = NULL;
g_hash_table_iter_init(&iter, providers);
while (g_hash_table_iter_next(&iter, &key, &value)) {
if (g_str_has_prefix((gchar*)key, search_str)) {
matches = g_list_append(matches, g_strdup((gchar*)key));
}
}
if (matches == NULL) {
return NULL;
}
/* Return first or last match based on previous flag */
gchar* result;
if (previous) {
result = g_strdup((gchar*)matches->data);
} else {
result = g_strdup((gchar*)g_list_last(matches)->data);
}
g_list_free_full(matches, g_free);
return result;
} }
gchar* gchar*