gemini-cli/packages/cli/src/ui/hooks/useSlashCompletion.ts

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,
};
}