summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/hooks/useSlashCompletion.ts
diff options
context:
space:
mode:
authorBryant Chandler <[email protected]>2025-08-05 16:18:03 -0700
committerGitHub <[email protected]>2025-08-05 23:18:03 +0000
commit12a9bc3ed94fab3071529b5304d46bcc5b4fe756 (patch)
tree90967b6670668c6c476719ac04422e1744cbabd6 /packages/cli/src/ui/hooks/useSlashCompletion.ts
parent2141b39c3d713a19f2dd8012a76c2ff8b7c30a5e (diff)
feat(core, cli): Introduce high-performance FileSearch engine (#5136)
Co-authored-by: Jacob Richman <[email protected]>
Diffstat (limited to 'packages/cli/src/ui/hooks/useSlashCompletion.ts')
-rw-r--r--packages/cli/src/ui/hooks/useSlashCompletion.ts187
1 files changed, 187 insertions, 0 deletions
diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.ts b/packages/cli/src/ui/hooks/useSlashCompletion.ts
new file mode 100644
index 00000000..9836362f
--- /dev/null
+++ b/packages/cli/src/ui/hooks/useSlashCompletion.ts
@@ -0,0 +1,187 @@
+/**
+ * @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,
+ };
+}