1/**2 * A DOM-free command palette model.3 *4 * Groups are objects with a `commands` array. A command may contain `id`,5 * `label`, `description`, `keywords`, `shortcut`, `disabled`, and `run`.6 * Group and command order is retained throughout the model's lifetime.7 */89const asText = value => value == null ? '' : String(value)1011function keywordsText(keywords) {12 if (Array.isArray(keywords)) return keywords.map(asText).join(' ')13 return asText(keywords)14}1516function searchableText(command) {17 return [18 command.id,19 command.label,20 command.description,21 keywordsText(command.keywords),22 command.shortcut,23 ].map(asText).join(' ').toLocaleLowerCase()24}2526function queryTokens(query) {27 return asText(query).toLocaleLowerCase().trim().split(/\s+/).filter(Boolean)28}2930function commandsForGroup(group) {31 if (Array.isArray(group)) return group32 return Array.isArray(group?.commands) ? group.commands : []33}3435/** Flatten groups while retaining references and stable source order. */36export function flattenCommandGroups(groups = []) {37 const source = Array.isArray(groups) ? groups : []38 return source.flatMap(group => commandsForGroup(group))39}4041/**42 * Create a command palette model. Methods return command objects (or null)43 * and never interact with the DOM.44 */45export function createCommandPaletteModel(groups = []) {46 const commands = flattenCommandGroups(groups)47 const byId = new Map()48 for (const command of commands) {49 if (command && command.id != null && !byId.has(command.id)) byId.set(command.id, command)50 }5152 let query = ''53 let selectedId = null5455 const matchingCommands = () => {56 const tokens = queryTokens(query)57 if (!tokens.length) return commands.filter(Boolean)58 return commands.filter(command => {59 if (!command) return false60 const text = searchableText(command)61 return tokens.every(token => text.includes(token))62 })63 }6465 const enabled = command => command && command.disabled !== true66 const enabledResults = () => matchingCommands().filter(enabled)6768 const select = command => {69 selectedId = command?.id ?? null70 return command ?? null71 }7273 const selectFirst = (nextQuery = query) => {74 query = asText(nextQuery)75 return select(enabledResults()[0])76 }7778 const move = (direction, nextQuery = query) => {79 query = asText(nextQuery)80 const results = matchingCommands()81 const available = results.filter(enabled)82 if (!available.length) return select(null)8384 const currentIndex = available.findIndex(command => command.id === selectedId)85 if (currentIndex < 0) return select(available[direction > 0 ? 0 : available.length - 1])86 const start = currentIndex87 return select(available[(start + direction + available.length) % available.length])88 }8990 return {91 groups,92 commands,93 get query() { return query },94 get selectedId() { return selectedId },95 get selected() { return byId.get(selectedId) ?? null },96 filter(nextQuery = '') {97 query = asText(nextQuery)98 return matchingCommands()99 },100 results() { return matchingCommands() },101 selectFirst,102 next: (nextQuery = query) => move(1, nextQuery),103 previous: (nextQuery = query) => move(-1, nextQuery),104 prev: (nextQuery = query) => move(-1, nextQuery),105 selectById(id) {106 const command = byId.get(id) ?? null107 return select(enabled(command) ? command : null)108 },109 getById(id) { return byId.get(id) ?? null },110 }111}112113export default createCommandPaletteModel