-
Notifications
You must be signed in to change notification settings - Fork 0
Autocomplete System
github-actions[bot] edited this page Sep 22, 2025
·
1 revision
@waforix/mocha provides a powerful and flexible autocomplete system for Discord slash commands, enabling dynamic suggestions based on user input and context.
The autocomplete system consists of:
- AutocompleteManager - Manages autocomplete handlers
- AutocompleteHandler - Functions that generate suggestions
- Helper Functions - Utilities for creating common autocomplete patterns
First, enable autocomplete on your command options:
import { SlashCommandBuilder } from '@waforix/mocha';
const searchCommand = new SlashCommandBuilder('search', 'Search for users or channels')
.addStringOption('query', 'Search query', true)
.setAutocomplete(true); // Enable autocompleteimport { Client } from '@waforix/mocha';
const client = new Client({ /* config */ });
// Register autocomplete handler
client.getAutocompleteManager().register('search', 'query', async (query, context) => {
// Return array of choices
return [
{ name: 'Option 1', value: 'option1' },
{ name: 'Option 2', value: 'option2' }
];
});For fixed lists of options:
import { createStaticChoices } from '@waforix/mocha';
const timeframes = [
{ name: '7 days', value: '7' },
{ name: '30 days', value: '30' },
{ name: '90 days', value: '90' },
{ name: 'All time', value: 'all' }
];
client.getAutocompleteManager().register(
'stats',
'timeframe',
createStaticChoices(timeframes)
);For searchable static lists:
import { createFilteredChoices } from '@waforix/mocha';
const categories = [
{ name: 'General Discussion', value: 'general' },
{ name: 'Gaming', value: 'gaming' },
{ name: 'Music', value: 'music' },
{ name: 'Technology', value: 'tech' },
{ name: 'Art & Design', value: 'art' }
];
client.getAutocompleteManager().register(
'category',
'name',
createFilteredChoices(categories)
);const customFilter = (choice, query) => {
// Custom matching logic
return choice.name.toLowerCase().startsWith(query.toLowerCase()) ||
choice.value.includes(query);
};
client.getAutocompleteManager().register(
'command',
'option',
createFilteredChoices(choices, customFilter)
);For database-driven or API-based suggestions:
import { createAsyncChoices } from '@waforix/mocha';
client.getAutocompleteManager().register(
'user',
'search',
createAsyncChoices(async (query, context) => {
if (!context.guildId || query.length < 2) {
return [];
}
// Get leaderboard data to find active users
const leaderboard = await client.getLeaderboard(context.guildId, 'messages', 25);
return leaderboard
.filter(user => user.username.toLowerCase().includes(query.toLowerCase()))
.map(user => ({
name: `${user.username} (${user.messageCount} messages)`,
value: user.userId
}));
})
);Use the context object to provide relevant suggestions:
client.getAutocompleteManager().register('channel', 'name', async (query, context) => {
if (!context.guildId) return [];
// Get channels from the current guild
const channels = await getGuildChannels(context.guildId);
return channels
.filter(channel =>
channel.name.toLowerCase().includes(query.toLowerCase())
)
.map(channel => ({
name: `#${channel.name}`,
value: channel.id
}))
.slice(0, 25); // Discord limit
});client.getAutocompleteManager().register('stats', 'user', async (query, context) => {
if (!context.guildId || query.length < 2) {
return [];
}
try {
// Get active users from leaderboard
const leaderboard = await client.getLeaderboard(context.guildId, 'messages', 25);
return leaderboard
.filter(user => user.username.toLowerCase().includes(query.toLowerCase()))
.map(user => ({
name: `${user.username} (${user.messageCount} messages)`,
value: user.userId
}));
} catch (error) {
console.error('User autocomplete error:', error);
return [];
}
});client.getAutocompleteManager().register('config', 'setting', async (query, context) => {
const availableSettings = [
{ name: 'Welcome Message', value: 'welcome_message' },
{ name: 'Auto Role', value: 'auto_role' },
{ name: 'Moderation Log', value: 'mod_log' },
{ name: 'Prefix', value: 'prefix' }
];
// Filter based on user permissions
const userPermissions = await getUserPermissions(context.userId, context.guildId);
return availableSettings
.filter(setting => {
// Only show admin settings to admins
if (setting.value.startsWith('mod_') && !userPermissions.includes('MANAGE_GUILD')) {
return false;
}
return setting.name.toLowerCase().includes(query.toLowerCase());
})
.slice(0, 25);
});For backward compatibility and specific use cases:
import { createAsyncChoicesHandler } from '@waforix/mocha';
const userFetcher = createAsyncChoicesHandler(async (guildId, query) => {
const users = await searchUsers(guildId, query);
return users.map(user => ({
name: user.displayName,
value: user.id
}));
});
client.getAutocompleteManager().register('command', 'user', userFetcher);client.getAutocompleteManager().register('search', 'query', async (query, context) => {
try {
if (!query || query.length < 2) {
return [
{ name: 'Type at least 2 characters to search', value: 'min_length' }
];
}
const results = await performSearch(query, context);
if (results.length === 0) {
return [
{ name: 'No results found', value: 'no_results' }
];
}
return results;
} catch (error) {
console.error('Autocomplete error:', error);
return [
{ name: 'Search temporarily unavailable', value: 'error' }
];
}
});const withTimeout = (promise, ms) => {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
]);
};
client.getAutocompleteManager().register('slow-search', 'query', async (query, context) => {
try {
const results = await withTimeout(
performSlowSearch(query, context),
2000 // 2 second timeout
);
return results;
} catch (error) {
if (error.message === 'Timeout') {
return [{ name: 'Search taking too long, try a more specific query', value: 'timeout' }];
}
return [{ name: 'Search error occurred', value: 'error' }];
}
});const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
client.getAutocompleteManager().register('cached-search', 'query', async (query, context) => {
const cacheKey = `${context.guildId}-${query}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const results = await performExpensiveSearch(query, context);
cache.set(cacheKey, {
data: results,
timestamp: Date.now()
});
return results;
});const debounceMap = new Map();
client.getAutocompleteManager().register('debounced-search', 'query', async (query, context) => {
const key = `${context.guildId}-${context.userId}`;
// Clear previous timeout
if (debounceMap.has(key)) {
clearTimeout(debounceMap.get(key));
}
return new Promise((resolve) => {
const timeout = setTimeout(async () => {
const results = await performSearch(query, context);
resolve(results);
debounceMap.delete(key);
}, 300); // 300ms debounce
debounceMap.set(key, timeout);
});
});- Limit Results - Discord allows max 25 choices
- Handle Empty Queries - Provide helpful default suggestions
- Use Meaningful Names - Make choice names descriptive
- Implement Caching - Cache expensive operations
- Handle Errors Gracefully - Always provide fallback options
- Optimize Performance - Keep response times under 3 seconds
- Validate Context - Check guild/user permissions
- Provide Feedback - Use choice names to guide users
// Command definition
const userCommand = new SlashCommandBuilder('userinfo', 'Get information about a user')
.addStringOption('user', 'Search for a user', true)
.setAutocomplete(true);
// Autocomplete handler
client.getAutocompleteManager().register('userinfo', 'user', async (query, context) => {
const leaderboard = await client.getLeaderboard(context.guildId, 'messages', 25);
return leaderboard
.filter(user => user.username.toLowerCase().includes(query.toLowerCase()))
.map(user => ({
name: `${user.username} (${user.messageCount} messages)`,
value: user.userId
}));
});
// Command handler
client.getCommandHandlerManager().register('userinfo', {
async execute(interaction) {
const userId = interaction.data.options?.[0]?.value;
const userInfo = await getUserInfo(userId);
return {
type: 4,
data: {
content: `**${userInfo.displayName}**\nJoined: ${userInfo.joinedAt}`
}
};
}
});For more information on building commands, see the Command System guide.