File size: 3,288 Bytes
5710dd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { useMemo, useState, useCallback } from "react";
import { useSettingsStore } from "@/stores";
import type { SlashCommandInfo } from "shared/legacy-sdk";

interface ActiveToken {
  trigger: "/" | "@";
  start: number;
  query: string;
}

function fuzzyMatch(text: string, query: string): boolean {
  if (!query) {
    return true;
  }
  const lowerText = text.toLowerCase();
  const lowerQuery = query.toLowerCase();
  let qi = 0;
  for (let i = 0; i < lowerText.length && qi < lowerQuery.length; i++) {
    if (lowerText[i] === lowerQuery[qi]) {
      qi++;
    }
  }
  return qi === lowerQuery.length;
}

export function findActiveToken(text: string, cursorPos: number): ActiveToken | null {
  const beforeCursor = text.slice(0, cursorPos);
  const lastSpace = Math.max(beforeCursor.lastIndexOf(" "), beforeCursor.lastIndexOf("\n"), beforeCursor.lastIndexOf("\t"), -1);
  const currentWord = beforeCursor.slice(lastSpace + 1);

  if (currentWord.startsWith("@")) {
    return { trigger: "@", start: lastSpace + 1, query: currentWord.slice(1) };
  }
  if (currentWord.startsWith("/")) {
    return { trigger: "/", start: lastSpace + 1, query: currentWord.slice(1) };
  }
  return null;
}

interface UseSlashMenuResult {
  showSlashMenu: boolean;
  filteredCommands: SlashCommandInfo[];
  selectedIndex: number;
  setSelectedIndex: (index: number) => void;
  handleSlashMenuKey: (e: React.KeyboardEvent) => boolean;
  resetSlashMenu: () => void;
}

export function useSlashMenu(activeToken: ActiveToken | null, onSelectCommand: (name: string) => void, onCancel: () => void): UseSlashMenuResult {
  const [selectedIndex, setSelectedIndex] = useState(0);
  const { slashCommands } = useSettingsStore();

  const showSlashMenu = activeToken?.trigger === "/";

  const filteredCommands = useMemo(() => {
    if (!showSlashMenu) {
      return [];
    }
    const q = activeToken.query;
    if (!q) {
      return slashCommands;
    }
    return slashCommands.filter((cmd) => fuzzyMatch(cmd.name, q) || fuzzyMatch(cmd.description, q));
  }, [showSlashMenu, activeToken?.query, slashCommands]);

  const resetSlashMenu = useCallback(() => {
    setSelectedIndex(0);
  }, []);

  const handleSlashMenuKey = useCallback(
    (e: React.KeyboardEvent): boolean => {
      if (!showSlashMenu || filteredCommands.length === 0) {
        return false;
      }

      switch (e.key) {
        case "ArrowDown":
          e.preventDefault();
          setSelectedIndex((i) => Math.min(i + 1, filteredCommands.length - 1));
          return true;
        case "ArrowUp":
          e.preventDefault();
          setSelectedIndex((i) => Math.max(i - 1, 0));
          return true;
        case "Tab":
        case "Enter": {
          e.preventDefault();
          const cmd = filteredCommands[selectedIndex];
          if (cmd) {
            onSelectCommand(cmd.name);
          }
          return true;
        }
        case "Escape":
          e.preventDefault();
          onCancel();
          return true;
        default:
          return false;
      }
    },
    [showSlashMenu, filteredCommands, selectedIndex, onSelectCommand, onCancel],
  );

  return {
    showSlashMenu,
    filteredCommands,
    selectedIndex,
    setSelectedIndex,
    handleSlashMenuKey,
    resetSlashMenu,
  };
}