188 lines
5.3 KiB
TypeScript
188 lines
5.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { Suggestion } from '../components/SuggestionsDisplay.js';
|
|
import { CommandContext, SlashCommand } from '../commands/types.js';
|
|
|
|
export interface UseSlashCompletionProps {
|
|
enabled: boolean;
|
|
query: string | null;
|
|
slashCommands: readonly SlashCommand[];
|
|
commandContext: CommandContext;
|
|
setSuggestions: (suggestions: Suggestion[]) => void;
|
|
setIsLoadingSuggestions: (isLoading: boolean) => void;
|
|
setIsPerfectMatch: (isMatch: boolean) => void;
|
|
}
|
|
|
|
export function useSlashCompletion(props: UseSlashCompletionProps): {
|
|
completionStart: number;
|
|
completionEnd: number;
|
|
} {
|
|
const {
|
|
enabled,
|
|
query,
|
|
slashCommands,
|
|
commandContext,
|
|
setSuggestions,
|
|
setIsLoadingSuggestions,
|
|
setIsPerfectMatch,
|
|
} = props;
|
|
const [completionStart, setCompletionStart] = useState(-1);
|
|
const [completionEnd, setCompletionEnd] = useState(-1);
|
|
|
|
useEffect(() => {
|
|
if (!enabled || query === null) {
|
|
return;
|
|
}
|
|
|
|
const fullPath = query?.substring(1) || '';
|
|
const hasTrailingSpace = !!query?.endsWith(' ');
|
|
const rawParts = fullPath.split(/\s+/).filter((p) => p);
|
|
let commandPathParts = rawParts;
|
|
let partial = '';
|
|
|
|
if (!hasTrailingSpace && rawParts.length > 0) {
|
|
partial = rawParts[rawParts.length - 1];
|
|
commandPathParts = rawParts.slice(0, -1);
|
|
}
|
|
|
|
let currentLevel: readonly SlashCommand[] | undefined = slashCommands;
|
|
let leafCommand: SlashCommand | null = null;
|
|
|
|
for (const part of commandPathParts) {
|
|
if (!currentLevel) {
|
|
leafCommand = null;
|
|
currentLevel = [];
|
|
break;
|
|
}
|
|
const found: SlashCommand | undefined = currentLevel.find(
|
|
(cmd) => cmd.name === part || cmd.altNames?.includes(part),
|
|
);
|
|
if (found) {
|
|
leafCommand = found;
|
|
currentLevel = found.subCommands as readonly SlashCommand[] | undefined;
|
|
} else {
|
|
leafCommand = null;
|
|
currentLevel = [];
|
|
break;
|
|
}
|
|
}
|
|
|
|
let exactMatchAsParent: SlashCommand | undefined;
|
|
if (!hasTrailingSpace && currentLevel) {
|
|
exactMatchAsParent = currentLevel.find(
|
|
(cmd) =>
|
|
(cmd.name === partial || cmd.altNames?.includes(partial)) &&
|
|
cmd.subCommands,
|
|
);
|
|
|
|
if (exactMatchAsParent) {
|
|
leafCommand = exactMatchAsParent;
|
|
currentLevel = exactMatchAsParent.subCommands;
|
|
partial = '';
|
|
}
|
|
}
|
|
|
|
setIsPerfectMatch(false);
|
|
if (!hasTrailingSpace) {
|
|
if (leafCommand && partial === '' && leafCommand.action) {
|
|
setIsPerfectMatch(true);
|
|
} else if (currentLevel) {
|
|
const perfectMatch = currentLevel.find(
|
|
(cmd) =>
|
|
(cmd.name === partial || cmd.altNames?.includes(partial)) &&
|
|
cmd.action,
|
|
);
|
|
if (perfectMatch) {
|
|
setIsPerfectMatch(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
const depth = commandPathParts.length;
|
|
const isArgumentCompletion =
|
|
leafCommand?.completion &&
|
|
(hasTrailingSpace ||
|
|
(rawParts.length > depth && depth > 0 && partial !== ''));
|
|
|
|
if (hasTrailingSpace || exactMatchAsParent) {
|
|
setCompletionStart(query.length);
|
|
setCompletionEnd(query.length);
|
|
} else if (partial) {
|
|
if (isArgumentCompletion) {
|
|
const commandSoFar = `/${commandPathParts.join(' ')}`;
|
|
const argStartIndex =
|
|
commandSoFar.length + (commandPathParts.length > 0 ? 1 : 0);
|
|
setCompletionStart(argStartIndex);
|
|
} else {
|
|
setCompletionStart(query.length - partial.length);
|
|
}
|
|
setCompletionEnd(query.length);
|
|
} else {
|
|
setCompletionStart(1);
|
|
setCompletionEnd(query.length);
|
|
}
|
|
|
|
if (isArgumentCompletion) {
|
|
const fetchAndSetSuggestions = async () => {
|
|
setIsLoadingSuggestions(true);
|
|
const argString = rawParts.slice(depth).join(' ');
|
|
const results =
|
|
(await leafCommand!.completion!(commandContext, argString)) || [];
|
|
const finalSuggestions = results.map((s) => ({ label: s, value: s }));
|
|
setSuggestions(finalSuggestions);
|
|
setIsLoadingSuggestions(false);
|
|
};
|
|
fetchAndSetSuggestions();
|
|
return;
|
|
}
|
|
|
|
const commandsToSearch = currentLevel || [];
|
|
if (commandsToSearch.length > 0) {
|
|
let potentialSuggestions = commandsToSearch.filter(
|
|
(cmd) =>
|
|
cmd.description &&
|
|
(cmd.name.startsWith(partial) ||
|
|
cmd.altNames?.some((alt) => alt.startsWith(partial))),
|
|
);
|
|
|
|
if (potentialSuggestions.length > 0 && !hasTrailingSpace) {
|
|
const perfectMatch = potentialSuggestions.find(
|
|
(s) => s.name === partial || s.altNames?.includes(partial),
|
|
);
|
|
if (perfectMatch && perfectMatch.action) {
|
|
potentialSuggestions = [];
|
|
}
|
|
}
|
|
|
|
const finalSuggestions = potentialSuggestions.map((cmd) => ({
|
|
label: cmd.name,
|
|
value: cmd.name,
|
|
description: cmd.description,
|
|
}));
|
|
|
|
setSuggestions(finalSuggestions);
|
|
return;
|
|
}
|
|
|
|
setSuggestions([]);
|
|
}, [
|
|
enabled,
|
|
query,
|
|
slashCommands,
|
|
commandContext,
|
|
setSuggestions,
|
|
setIsLoadingSuggestions,
|
|
setIsPerfectMatch,
|
|
]);
|
|
|
|
return {
|
|
completionStart,
|
|
completionEnd,
|
|
};
|
|
}
|