You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

registry.ts 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import type { SlashCommandHandler } from './types'
  2. import type { CommandSearchResult } from '../types'
  3. /**
  4. * Slash Command Registry System
  5. * Responsible for managing registration, lookup, and search of all slash commands
  6. */
  7. export class SlashCommandRegistry {
  8. private commands = new Map<string, SlashCommandHandler>()
  9. private commandDeps = new Map<string, any>()
  10. /**
  11. * Register command handler
  12. */
  13. register<TDeps = any>(handler: SlashCommandHandler<TDeps>, deps?: TDeps) {
  14. // Register main command name
  15. this.commands.set(handler.name, handler)
  16. // Register aliases
  17. if (handler.aliases) {
  18. handler.aliases.forEach((alias) => {
  19. this.commands.set(alias, handler)
  20. })
  21. }
  22. // Store dependencies and call registration method
  23. if (deps) {
  24. this.commandDeps.set(handler.name, deps)
  25. handler.register?.(deps)
  26. }
  27. }
  28. /**
  29. * Unregister command
  30. */
  31. unregister(name: string) {
  32. const handler = this.commands.get(name)
  33. if (handler) {
  34. // Call the command's unregister method
  35. handler.unregister?.()
  36. // Remove dependencies
  37. this.commandDeps.delete(handler.name)
  38. // Remove main command name
  39. this.commands.delete(handler.name)
  40. // Remove all aliases
  41. if (handler.aliases) {
  42. handler.aliases.forEach((alias) => {
  43. this.commands.delete(alias)
  44. })
  45. }
  46. }
  47. }
  48. /**
  49. * Find command handler
  50. */
  51. findCommand(commandName: string): SlashCommandHandler | undefined {
  52. return this.commands.get(commandName)
  53. }
  54. /**
  55. * Smart partial command matching
  56. * Prioritize alias matching, then match command name prefix
  57. */
  58. private findBestPartialMatch(partialName: string): SlashCommandHandler | undefined {
  59. const lowerPartial = partialName.toLowerCase()
  60. // First check if any alias starts with this
  61. const aliasMatch = this.findHandlerByAliasPrefix(lowerPartial)
  62. if (aliasMatch)
  63. return aliasMatch
  64. // Then check if command name starts with this
  65. return this.findHandlerByNamePrefix(lowerPartial)
  66. }
  67. /**
  68. * Find handler by alias prefix
  69. */
  70. private findHandlerByAliasPrefix(prefix: string): SlashCommandHandler | undefined {
  71. for (const handler of this.getAllCommands()) {
  72. if (handler.aliases?.some(alias => alias.toLowerCase().startsWith(prefix)))
  73. return handler
  74. }
  75. return undefined
  76. }
  77. /**
  78. * Find handler by name prefix
  79. */
  80. private findHandlerByNamePrefix(prefix: string): SlashCommandHandler | undefined {
  81. return this.getAllCommands().find(handler =>
  82. handler.name.toLowerCase().startsWith(prefix),
  83. )
  84. }
  85. /**
  86. * Get all registered commands (deduplicated)
  87. */
  88. getAllCommands(): SlashCommandHandler[] {
  89. const uniqueCommands = new Map<string, SlashCommandHandler>()
  90. this.commands.forEach((handler) => {
  91. uniqueCommands.set(handler.name, handler)
  92. })
  93. return Array.from(uniqueCommands.values())
  94. }
  95. /**
  96. * Search commands
  97. * @param query Full query (e.g., "/theme dark" or "/lang en")
  98. * @param locale Current language
  99. */
  100. async search(query: string, locale: string = 'en'): Promise<CommandSearchResult[]> {
  101. const trimmed = query.trim()
  102. // Handle root level search "/"
  103. if (trimmed === '/' || !trimmed.replace('/', '').trim())
  104. return await this.getRootCommands()
  105. // Parse command and arguments
  106. const afterSlash = trimmed.substring(1).trim()
  107. const spaceIndex = afterSlash.indexOf(' ')
  108. const commandName = spaceIndex === -1 ? afterSlash : afterSlash.substring(0, spaceIndex)
  109. const args = spaceIndex === -1 ? '' : afterSlash.substring(spaceIndex + 1).trim()
  110. // First try exact match
  111. let handler = this.findCommand(commandName)
  112. if (handler) {
  113. try {
  114. return await handler.search(args, locale)
  115. }
  116. catch (error) {
  117. console.warn(`Command search failed for ${commandName}:`, error)
  118. return []
  119. }
  120. }
  121. // If no exact match, try smart partial matching
  122. handler = this.findBestPartialMatch(commandName)
  123. if (handler) {
  124. try {
  125. return await handler.search(args, locale)
  126. }
  127. catch (error) {
  128. console.warn(`Command search failed for ${handler.name}:`, error)
  129. return []
  130. }
  131. }
  132. // Finally perform fuzzy search
  133. return this.fuzzySearchCommands(afterSlash)
  134. }
  135. /**
  136. * Get root level command list
  137. */
  138. private async getRootCommands(): Promise<CommandSearchResult[]> {
  139. const results: CommandSearchResult[] = []
  140. // Generate a root level item for each command
  141. for (const handler of this.getAllCommands()) {
  142. results.push({
  143. id: `root-${handler.name}`,
  144. title: `/${handler.name}`,
  145. description: handler.description,
  146. type: 'command' as const,
  147. data: {
  148. command: `root.${handler.name}`,
  149. args: { name: handler.name },
  150. },
  151. })
  152. }
  153. return results
  154. }
  155. /**
  156. * Fuzzy search commands
  157. */
  158. private fuzzySearchCommands(query: string): CommandSearchResult[] {
  159. const lowercaseQuery = query.toLowerCase()
  160. const matches: CommandSearchResult[] = []
  161. this.getAllCommands().forEach((handler) => {
  162. // Check if command name matches
  163. if (handler.name.toLowerCase().includes(lowercaseQuery)) {
  164. matches.push({
  165. id: `fuzzy-${handler.name}`,
  166. title: `/${handler.name}`,
  167. description: handler.description,
  168. type: 'command' as const,
  169. data: {
  170. command: `root.${handler.name}`,
  171. args: { name: handler.name },
  172. },
  173. })
  174. }
  175. // Check if aliases match
  176. if (handler.aliases) {
  177. handler.aliases.forEach((alias) => {
  178. if (alias.toLowerCase().includes(lowercaseQuery)) {
  179. matches.push({
  180. id: `fuzzy-${alias}`,
  181. title: `/${alias}`,
  182. description: `${handler.description} (alias for /${handler.name})`,
  183. type: 'command' as const,
  184. data: {
  185. command: `root.${handler.name}`,
  186. args: { name: handler.name },
  187. },
  188. })
  189. }
  190. })
  191. }
  192. })
  193. return matches
  194. }
  195. /**
  196. * Get command dependencies
  197. */
  198. getCommandDependencies(commandName: string): any {
  199. return this.commandDeps.get(commandName)
  200. }
  201. }
  202. // Global registry instance
  203. export const slashCommandRegistry = new SlashCommandRegistry()