/********************************************************************************
 * Copyright (C) 2017 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/ import { injectable , inject , named } from 'inversify' ; import { Event , Emitter , WaitUntilEvent } from './event' ; import { Disposable , DisposableCollection } from './disposable' ; import { ContributionProvider } from './contribution-provider' ; import { nls } from './nls' ; import debounce = require ( 'p-debounce' ) ; /**
 * A command is a unique identifier of a function
 * which can be executed by a user via a keyboard shortcut,
 * a menu action or directly.
 */ export interface Command { /**
     * A unique identifier of this command.
     */ id : string ; /**
     * A label of this command.
     */ label ? : string ; originalLabel ? : string ; /**
     * An icon class of this command.
     */ iconClass ? : string ; /**
     * A category of this command.
     */ category ? : string ; originalCategory ? : string ; } export namespace Command { export function is ( arg ) { return ! ! arg && arg === Object ( arg ) && 'id' in arg ; } /** Utility function to easily translate commands */ export function toLocalizedCommand ( command , nlsLabelKey = command . id , nlsCategoryKey ? ) { return { ... command , label : command . label && nls . localize ( nlsLabelKey , command . label ) , originalLabel : command . label , category : nlsCategoryKey && command . category && nls . localize ( nlsCategoryKey , command . category ) || command . category , originalCategory : command . category , } ; } export function toDefaultLocalizedCommand ( command ) { return { ... command , label : command . label && nls . localizeByDefault ( command . label ) , originalLabel : command . label , category : command . category && nls . localizeByDefault ( command . category ) , originalCategory : command . category , } ; } /** Comparator function for when sorting commands */ export function compareCommands ( a , b ) { if ( a . label && b . label ) { const aCommand = ( a . category ? `${ a . category }: ${ a . label }` : a . label ) . toLowerCase ( ) ; const bCommand = ( b . category ? `${ b . category }: ${ b . label }` : b . label ) . toLowerCase ( ) ; return ( aCommand ) . localeCompare ( bCommand ) ; } else { return 0 ; } } param a param b export function equals ( a , b ) { return ( a . id === b . id && a . label === b . label && a . iconClass === b . iconClass && a . category === b . category ) ; } } /**
 * A command handler is an implementation of a command.
 *
 * A command can have multiple handlers
 * but they should be active in different contexts,
 * otherwise first active will be executed.
 */ export interface CommandHandler { /**
     * Execute this handler.
     *
     * Don't call it directly, use `CommandService.executeCommand` instead.
     */ execute ( ... args ) : any ; /**
     * Test whether this handler is enabled (active).
     */ isEnabled ? ( ... args ) : boolean ; /**
     * Test whether menu items for this handler should be visible.
     */ isVisible ? ( ... args ) : boolean ; /**
     * Test whether menu items for this handler should be toggled.
     */ isToggled ? ( ... args ) : boolean ; } export const CommandContribution = Symbol ( 'CommandContribution' ) ; /**
 * The command contribution should be implemented to register custom commands and handler.
 */ export interface CommandContribution { /**
     * Register commands and handlers.
     */ registerCommands ( commands ) : void ; } export interface CommandEvent { commandId : string ; args : any [ ] ; } export interface WillExecuteCommandEvent extends WaitUntilEvent , CommandEvent { } export const commandServicePath = '/services/commands' ; export const CommandService = Symbol ( 'CommandService' ) ; /**
 * The command service should be used to execute commands.
 */ export interface CommandService { /**
     * Execute the active handler for the given command and arguments.
     *
     * Reject if a command cannot be executed.
     */ executeCommand < T > ( command , ... args ) : Promise < T | undefined > ; /**
     * An event is emitted when a command is about to be executed.
     *
     * It can be used to install or activate a command handler.
     */ readonly onWillExecuteCommand : Event < WillExecuteCommandEvent > ; /**
     * An event is emitted when a command was executed.
     */ readonly onDidExecuteCommand : Event < CommandEvent > ; } /**
 * The command registry manages commands and handlers.
 */ @ injectable ( ) export class CommandRegistry implements CommandService { protected readonly _commands : { [ id ] : Command ; } = { } ; protected readonly _handlers : { [ id ] : CommandHandler [ ] ; } = { } ; protected readonly toUnregisterCommands = new Map < string , Disposable > ( ) ; protected _recent : Command [ ] = [ ] ; protected readonly onWillExecuteCommandEmitter = new Emitter < WillExecuteCommandEvent > ( ) ; readonly onWillExecuteCommand = this . onWillExecuteCommandEmitter . event ; protected readonly onDidExecuteCommandEmitter = new Emitter < CommandEvent > ( ) ; readonly onDidExecuteCommand = this . onDidExecuteCommandEmitter . event ; protected readonly onCommandsChangedEmitter = new Emitter < void > ( ) ; readonly onCommandsChanged = this . onCommandsChangedEmitter . event ; constructor ( @ inject ( ContributionProvider ) @ named ( CommandContribution ) protected readonly contributionProvider ) { } onStart ( ) { const contributions = this . contributionProvider . getContributions ( ) ; for ( const contrib of contributions ) { contrib . registerCommands ( this ) ; } } * getAllCommands ( ) { for ( const command of Object . values ( this . _commands ) ) { yield { ... command , handlers : this . _handlers [ command . id ] ?? [ ] } ; } } /**
     * Register the given command and handler if present.
     *
     * Throw if a command is already registered for the given command identifier.
     */ registerCommand ( command , handler ? ) { if ( this . _commands [ command . id ] ) { console . warn ( `A command ${ command . id } is already registered.` ) ; return Disposable . NULL ; } const toDispose = new DisposableCollection ( this . doRegisterCommand ( command ) ) ; if ( handler ) { toDispose . push ( this . registerHandler ( command . id , handler ) ) ; } this . toUnregisterCommands . set ( command . id , toDispose ) ; toDispose . push ( Disposable . create ( ( ) => this . toUnregisterCommands . delete ( command . id ) ) ) ; return toDispose ; } protected doRegisterCommand ( command ) { this . _commands [ command . id ] = command ; return { dispose : ( ) => { delete this . _commands [ command . id ] ; } } ; } param command unregisterCommand ( command ) ; param id unregisterCommand ( id ) ; unregisterCommand ( commandOrId ) { const id = Command . is ( commandOrId ) ? commandOrId . id : commandOrId ; const toUnregister = this . toUnregisterCommands . get ( id ) ; if ( toUnregister ) { toUnregister . dispose ( ) ; } } /**
     * Register the given handler for the given command identifier.
     *
     * If there is already a handler for the given command
     * then the given handler is registered as more specific, and
     * has higher priority during enablement, visibility and toggle state evaluations.
     */ registerHandler ( commandId , handler ) { let handlers = this . _handlers [ commandId ] ; if ( ! handlers ) { this . _handlers [ commandId ] = handlers = [ ] ; } handlers . unshift ( handler ) ; this . fireDidChange ( ) ; return { dispose : ( ) => { const idx = handlers . indexOf ( handler ) ; if ( idx >= 0 ) { handlers . splice ( idx , 1 ) ; this . fireDidChange ( ) ; } } } ; } protected fireDidChange = debounce ( ( ) => this . doFireDidChange ( ) , 0 ) ; protected doFireDidChange ( ) { this . onCommandsChangedEmitter . fire ( ) ; } /**
     * Test whether there is an active handler for the given command.
     */ isEnabled ( command , ... args ) { return typeof this . getActiveHandler ( command , ... args ) !== 'undefined' ; } /**
     * Test whether there is a visible handler for the given command.
     */ isVisible ( command , ... args ) { return typeof this . getVisibleHandler ( command , ... args ) !== 'undefined' ; } /**
     * Test whether there is a toggled handler for the given command.
     */ isToggled ( command , ... args ) { return typeof this . getToggledHandler ( command , ... args ) !== 'undefined' ; } /**
     * Execute the active handler for the given command and arguments.
     *
     * Reject if a command cannot be executed.
     */ async executeCommand < T > ( commandId , ... args ) { const handler = this . getActiveHandler ( commandId , ... args ) ; if ( handler ) { await this . fireWillExecuteCommand ( commandId , args ) ; const result = await handler . execute ( ... args ) ; this . onDidExecuteCommandEmitter . fire ( { commandId , args } ) ; return result ; } throw Object . assign ( new Error ( `The command '${ commandId }' cannot be executed. There are no active handlers available for the command.` ) , { code : 'NO_ACTIVE_HANDLER' } ) ; } protected async fireWillExecuteCommand ( commandId , args = [ ] ) { await WaitUntilEvent . fire ( this . onWillExecuteCommandEmitter , { commandId , args } , 30000 ) ; } /**
     * Get a visible handler for the given command or `undefined`.
     */ getVisibleHandler ( commandId , ... args ) { const handlers = this . _handlers [ commandId ] ; if ( handlers ) { for ( const handler of handlers ) { try { if ( ! handler . isVisible || handler . isVisible ( ... args ) ) { return handler ; } } catch ( error ) { console . error ( error ) ; } } } return undefined ; } /**
     * Get an active handler for the given command or `undefined`.
     */ getActiveHandler ( commandId , ... args ) { const handlers = this . _handlers [ commandId ] ; if ( handlers ) { for ( const handler of handlers ) { try { if ( ! handler . isEnabled || handler . isEnabled ( ... args ) ) { return handler ; } } catch ( error ) { console . error ( error ) ; } } } return undefined ; } /**
     * Get a toggled handler for the given command or `undefined`.
     */ getToggledHandler ( commandId , ... args ) { const handlers = this . _handlers [ commandId ] ; if ( handlers ) { for ( const handler of handlers ) { try { if ( handler . isToggled && handler . isToggled ( ... args ) ) { return handler ; } } catch ( error ) { console . error ( error ) ; } } } return undefined ; } /**
     * Returns with all handlers for the given command. If the command does not have any handlers,
     * or the command is not registered, returns an empty array.
     */ getAllHandlers ( commandId ) { const handlers = this . _handlers [ commandId ] ; return handlers ? handlers . slice ( ) : [ ] ; } /**
     * Get all registered commands.
     */ get commands ( ) : Command [ ] { const commands = [ ] ; for ( const id of this . commandIds ) { const cmd = this . getCommand ( id ) ; if ( cmd ) { commands . push ( cmd ) ; } } return commands ; } /**
     * Get a command for the given command identifier.
     */ getCommand ( id ) { return this . _commands [ id ] ; } /**
     * Get all registered commands identifiers.
     */ get commandIds ( ) : string [ ] { return Object . keys ( this . _commands ) ; } /**
     * Get the list of recently used commands.
     */ get recent ( ) : Command [ ] { return this . _recent ; } param commands set recent ( commands ) { this . _recent = commands ; } param recent addRecentCommand ( recent ) { if ( Array . isArray ( recent ) ) { recent . forEach ( ( command ) => this . addRecentCommand ( command ) ) ; } else { const index = this . _recent . findIndex ( ( command ) => Command . equals ( recent , command ) ) ; if ( index >= 0 ) { this . _recent . splice ( index , 1 ) ; } this . _recent . unshift ( recent ) ; } } /**
     * Clear the list of recently used commands.
     */ clearCommandHistory ( ) { this . recent = [ ] ; } }