import { pluginCommandKeybindingActionId, type PluginCommandAliasActionId } from '../../shared/plugins/plugin-command-actions' import { getKeybindingConflictIdentity, type KeybindingOverrides } from '../../shared/keybindings' import type { PluginCommandContribution, PluginManifest } from '../../shared/plugins/plugin-manifest' import type { PluginKeybindingContribution } from '../../shared/plugins/plugin-content-pack-contributions' import { isInvalidDiscoveredPlugin, type DiscoveredPlugin, type ValidDiscoveredPlugin } from './plugin-discovery' export type PluginCommandKeybinding = { key: string when: 'global' | 'worktree' } export type PluginCommandRegistration = { pluginKey: string id: string title: string context: 'global' | 'worktree' handler: { type: 'built-in'; action: PluginCommandAliasActionId } | { type: 'worker' } keybindings: PluginCommandKeybinding[] } type CommandOwner = { pluginKey: string key: string } export class PluginCommandRegistry { private active: PluginCommandRegistration[] = [] private readonly previews = new Map() private readonly errors = new Map() list(): readonly PluginCommandRegistration[] { return this.active } preview(pluginKey: string): readonly PluginCommandRegistration[] { return this.previews.get(pluginKey) ?? [] } error(pluginKey: string): string | null { return this.errors.get(pluginKey) ?? null } reconcile( discovered: readonly DiscoveredPlugin[], isApproved: (plugin: ValidDiscoveredPlugin) => boolean, overrides: KeybindingOverrides = {}, platform: NodeJS.Platform = process.platform ): void { const candidates = discovered.filter( (plugin): plugin is ValidDiscoveredPlugin => !isInvalidDiscoveredPlugin(plugin) && plugin.manifest.contributes.commands.length > 0 ) const registrations = candidates.map((plugin) => ({ pluginKey: plugin.pluginKey, approved: isApproved(plugin), commands: registrationsForManifest(plugin.pluginKey, plugin.manifest) })) this.previews.clear() this.errors.clear() for (const plugin of registrations) { this.previews.set(plugin.pluginKey, plugin.commands) } const approved = registrations.filter((plugin) => plugin.approved) const chordOwners = new Map() for (const plugin of approved) { for (const command of plugin.commands) { for (const keybinding of effectiveCommandKeybindings(command, overrides)) { const identity = getKeybindingConflictIdentity(keybinding.key, platform) const owners = chordOwners.get(identity) ?? [] owners.push({ pluginKey: plugin.pluginKey, key: keybinding.key }) chordOwners.set(identity, owners) } } } const conflicted = new Set() for (const owners of chordOwners.values()) { // Global/worktree are the only contexts, so all owners of the same chord overlap. if (owners.length < 2) { continue } for (const owner of owners) { conflicted.add(owner.pluginKey) this.errors.set( owner.pluginKey, `plugin keybinding ${owner.key} conflicts with another plugin` ) } } this.active = approved .filter((plugin) => !conflicted.has(plugin.pluginKey)) .flatMap((plugin) => plugin.commands) } } function effectiveCommandKeybindings( command: PluginCommandRegistration, overrides: KeybindingOverrides ): PluginCommandKeybinding[] { const override = overrides[pluginCommandKeybindingActionId(command.pluginKey, command.id)] if (!Array.isArray(override)) { return command.keybindings } return override.map((key) => ({ key, when: command.context })) } function registrationsForManifest( pluginKey: string, manifest: PluginManifest ): PluginCommandRegistration[] { const bindingsByCommand = new Map() for (const binding of manifest.contributes.keybindings) { const commandId = binding.command const bindings = bindingsByCommand.get(commandId) if (bindings) { bindings.push(binding) } else { bindingsByCommand.set(commandId, [binding]) } } return manifest.contributes.commands.map((command) => ({ pluginKey, id: command.id, title: command.title, context: command.context ?? 'global', handler: command.action === undefined ? { type: 'worker' as const } : { type: 'built-in' as const, action: command.action as PluginCommandAliasActionId }, keybindings: keybindingsForCommand(command, bindingsByCommand.get(command.id) ?? []) })) } function keybindingsForCommand( command: PluginCommandContribution, keybindings: readonly PluginKeybindingContribution[] ): PluginCommandKeybinding[] { return keybindings.map((keybinding) => ({ key: keybinding.key, when: keybinding.when ?? command.context ?? 'global' })) }