{"version":3,"file":"filterCommands.mjs","names":[],"sources":["../../../../package/common/commandPalette/filterCommands.ts"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { isMenuAction, isMenuHeading, type MenuAction, type MenuEntry } from '../menu/menu.types';\n\nexport interface CommandGroup {\n  heading?: ReactNode;\n  /** Whether a separator started this group, so the divider can be drawn above it. */\n  separated: boolean;\n  actions: MenuAction[];\n}\n\n// Lower-cased and stripped of accents, so \"uber\" finds \"Übersicht\".\nfunction normalize(text: string): string {\n  return text\n    .normalize('NFD')\n    .replace(/\\p{Diacritic}/gu, '')\n    .toLowerCase();\n}\n\n/** Splits entries into groups at each heading and separator, keeping their order. */\nexport function groupCommands(items: MenuEntry[]): CommandGroup[] {\n  const groups: CommandGroup[] = [];\n  let current: CommandGroup = { separated: false, actions: [] };\n  const flush = () => {\n    if (current.heading !== undefined || current.actions.length > 0) groups.push(current);\n  };\n  for (const entry of items) {\n    if (isMenuAction(entry)) {\n      current.actions.push(entry);\n      continue;\n    }\n    flush();\n    current = isMenuHeading(entry)\n      ? { heading: entry.heading, separated: current.separated && current.actions.length === 0, actions: [] }\n      : { separated: true, actions: [] };\n  }\n  flush();\n  return groups.filter((group) => group.actions.length > 0);\n}\n\n/**\n * 0 when the label starts with the query, 1 when a word of the label or a keyword does, 2 for any\n * other match, `null` when a query word is missing from both the label and the keywords.\n *\n * A label that is not a string cannot be read, so such an action is found through its keywords only.\n */\nfunction rank(action: MenuAction, words: string[], query: string): number | null {\n  const label = typeof action.label === 'string' ? normalize(action.label) : '';\n  const keywords = (action.keywords ?? []).map(normalize);\n  const haystack = [label, ...keywords].join(' ');\n  if (!words.every((word) => haystack.includes(word))) return null;\n  if (label.startsWith(query)) return 0;\n  const hasWordStartingWith = (text: string) => text.split(/\\s+/).some((word) => word.startsWith(words[0]));\n  if (hasWordStartingWith(label) || keywords.some(hasWordStartingWith)) return 1;\n  return 2;\n}\n\ninterface RankedGroup {\n  group: CommandGroup;\n  best: number;\n}\n\n/**\n * The groups with only the actions matching `query`, best matches first within each group, and\n * the group holding the best match first. A heading moves with its group, never away from it.\n */\nexport function filterCommands(groups: CommandGroup[], query: string): CommandGroup[] {\n  const normalized = normalize(query.trim());\n  if (!normalized) return groups;\n  const words = normalized.split(/\\s+/);\n\n  // Both sorts rely on `Array.prototype.sort` being stable, so ties keep the order they were given in.\n  return groups\n    .map((group): RankedGroup | null => {\n      const matches = group.actions\n        .map((action) => ({ action, rank: rank(action, words, normalized) }))\n        .filter((match): match is { action: MenuAction; rank: number } => match.rank !== null)\n        .sort((a, b) => a.rank - b.rank);\n      if (matches.length === 0) return null;\n      return {\n        // Separators are dropped while filtering: the headings still say where one group ends.\n        group: { ...group, separated: false, actions: matches.map((match) => match.action) },\n        best: matches[0].rank,\n      };\n    })\n    .filter((ranked): ranked is RankedGroup => ranked !== null)\n    .sort((a, b) => a.best - b.best)\n    .map((ranked) => ranked.group);\n}\n"],"mappings":"sFAWA,SAAS,EAAU,EAAsB,CACvC,OAAO,EACJ,UAAU,KAAK,CAAC,CAChB,QAAQ,kBAAmB,EAAE,CAAC,CAC9B,YAAY,CACjB,CAGA,SAAgB,EAAc,EAAoC,CAChE,IAAM,EAAyB,CAAC,EAC5B,EAAwB,CAAE,UAAW,GAAO,QAAS,CAAC,CAAE,EACtD,MAAc,EACd,EAAQ,UAAY,IAAA,IAAa,EAAQ,QAAQ,OAAS,IAAG,EAAO,KAAK,CAAO,CACtF,EACA,IAAK,IAAM,KAAS,EAAO,CACzB,GAAI,EAAa,CAAK,EAAG,CACvB,EAAQ,QAAQ,KAAK,CAAK,EAC1B,QACF,CACA,EAAM,EACN,EAAU,EAAc,CAAK,EACzB,CAAE,QAAS,EAAM,QAAS,UAAW,EAAQ,WAAa,EAAQ,QAAQ,SAAW,EAAG,QAAS,CAAC,CAAE,EACpG,CAAE,UAAW,GAAM,QAAS,CAAC,CAAE,CACrC,CAEA,OADA,EAAM,EACC,EAAO,OAAQ,GAAU,EAAM,QAAQ,OAAS,CAAC,CAC1D,CAQA,SAAS,EAAK,EAAoB,EAAiB,EAA8B,CAC/E,IAAM,EAAQ,OAAO,EAAO,OAAU,SAAW,EAAU,EAAO,KAAK,EAAI,GACrE,GAAY,EAAO,UAAY,CAAC,EAAA,CAAG,IAAI,CAAS,EAChD,EAAW,CAAC,EAAO,GAAG,CAAQ,CAAC,CAAC,KAAK,GAAG,EAC9C,GAAI,CAAC,EAAM,MAAO,GAAS,EAAS,SAAS,CAAI,CAAC,EAAG,OAAO,KAC5D,GAAI,EAAM,WAAW,CAAK,EAAG,MAAO,GACpC,IAAM,EAAuB,GAAiB,EAAK,MAAM,KAAK,CAAC,CAAC,KAAM,GAAS,EAAK,WAAW,EAAM,EAAE,CAAC,EAExG,OADI,EAAoB,CAAK,GAAK,EAAS,KAAK,CAAmB,EAAU,EACtE,CACT,CAWA,SAAgB,EAAe,EAAwB,EAA+B,CACpF,IAAM,EAAa,EAAU,EAAM,KAAK,CAAC,EACzC,GAAI,CAAC,EAAY,OAAO,EACxB,IAAM,EAAQ,EAAW,MAAM,KAAK,EAGpC,OAAO,EACJ,IAAK,GAA8B,CAClC,IAAM,EAAU,EAAM,QACnB,IAAK,IAAY,CAAE,SAAQ,KAAM,EAAK,EAAQ,EAAO,CAAU,CAAE,EAAE,CAAC,CACpE,OAAQ,GAAyD,EAAM,OAAS,IAAI,CAAC,CACrF,MAAM,EAAG,IAAM,EAAE,KAAO,EAAE,IAAI,EAEjC,OADI,EAAQ,SAAW,EAAU,KAC1B,CAEL,MAAO,CAAE,GAAG,EAAO,UAAW,GAAO,QAAS,EAAQ,IAAK,GAAU,EAAM,MAAM,CAAE,EACnF,KAAM,EAAQ,EAAE,CAAC,IACnB,CACF,CAAC,CAAC,CACD,OAAQ,GAAkC,IAAW,IAAI,CAAC,CAC1D,MAAM,EAAG,IAAM,EAAE,KAAO,EAAE,IAAI,CAAC,CAC/B,IAAK,GAAW,EAAO,KAAK,CACjC"}