/********************************************************************************
 * 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 { isOSX } from '../common/os' ; import { Emitter , Event } from '../common/event' ; import { CommandRegistry , Command } from '../common/command' ; import { Disposable , DisposableCollection } from '../common/disposable' ; import { KeyCode , KeySequence , Key } from './keyboard/keys' ; import { KeyboardLayoutService } from './keyboard/keyboard-layout-service' ; import { ContributionProvider } from '../common/contribution-provider' ; import { ILogger } from '../common/logger' ; import { StatusBarAlignment , StatusBar } from './status-bar/status-bar' ; import { ContextKeyService } from './context-key-service' ; import { CorePreferences } from './core-preferences' ; import * as common from '../common/keybinding' ; export enum KeybindingScope { DEFAULT , USER , WORKSPACE , END } export namespace KeybindingScope { export const length = KeybindingScope . END - KeybindingScope . DEFAULT ; } deprecated export type Keybinding = common . Keybinding ; export const Keybinding = common . Keybinding ; export interface ResolvedKeybinding extends common . Keybinding { /**
     * The KeyboardLayoutService may transform the `keybinding` depending on the
     * user's keyboard layout. This property holds the transformed keybinding that
     * should be used in the UI. The value is undefined if the KeyboardLayoutService
     * has not been called yet to resolve the keybinding.
     */ resolved ? : KeyCode [ ] ; } export interface ScopedKeybinding extends common . Keybinding { /** Current keybinding scope */ scope : KeybindingScope ; } export const KeybindingContribution = Symbol ( 'KeybindingContribution' ) ; /**
 * Allows extensions to contribute  common . Keybinding s
  export interface KeybindingContribution { param keybindings registerKeybindings ( keybindings ) : void ; } export const KeybindingContext = Symbol ( 'KeybindingContext' ) ; export interface KeybindingContext { /**
     * The unique ID of the current context.
     */ readonly id : string ; isEnabled ( arg ) : boolean ; } export namespace KeybindingContexts { export const NOOP_CONTEXT = { id : 'noop.keybinding.context' , isEnabled : ( ) => true } ; export const DEFAULT_CONTEXT = { id : 'default.keybinding.context' , isEnabled : ( ) => false } ; } @ injectable ( ) export class KeybindingRegistry { static readonly PASSTHROUGH_PSEUDO_COMMAND = 'passthrough' ; protected keySequence : KeySequence = [ ] ; protected readonly contexts : { [ id ] : KeybindingContext ; } = { } ; protected readonly keymaps : ScopedKeybinding [ ] [ ] = [ ... Array ( KeybindingScope . length ) ] . map ( ( ) => [ ] ) ; @ inject ( CorePreferences ) protected readonly corePreferences : CorePreferences ; @ inject ( KeyboardLayoutService ) protected readonly keyboardLayoutService : KeyboardLayoutService ; @ inject ( ContributionProvider ) @ named ( KeybindingContext ) protected readonly contextProvider : ContributionProvider < KeybindingContext > ; @ inject ( CommandRegistry ) protected readonly commandRegistry : CommandRegistry ; @ inject ( ContributionProvider ) @ named ( KeybindingContribution ) protected readonly contributions : ContributionProvider < KeybindingContribution > ; @ inject ( StatusBar ) protected readonly statusBar : StatusBar ; @ inject ( ILogger ) protected readonly logger : ILogger ; @ inject ( ContextKeyService ) protected readonly whenContextService : ContextKeyService ; async onStart ( ) { await this . keyboardLayoutService . initialize ( ) ; this . keyboardLayoutService . onKeyboardLayoutChanged ( newLayout => { this . clearResolvedKeybindings ( ) ; this . keybindingsChanged . fire ( undefined ) ; } ) ; this . registerContext ( KeybindingContexts . NOOP_CONTEXT ) ; this . registerContext ( KeybindingContexts . DEFAULT_CONTEXT ) ; this . registerContext ( ... this . contextProvider . getContributions ( ) ) ; for ( const contribution of this . contributions . getContributions ( ) ) { contribution . registerKeybindings ( this ) ; } } protected keybindingsChanged = new Emitter < void > ( ) ; /**
     * Event that is fired when the resolved keybindings change due to a different keyboard layout
     * or when a new keymap is being set
     */ get onKeybindingsChanged ( ) : Event < void > { return this . keybindingsChanged . event ; } param contexts protected registerContext ( ... contexts ) { for ( const context of contexts ) { const { id } = context ; if ( this . contexts [ id ] ) { this . logger . error ( `A keybinding context with ID ${ id } is already registered.` ) ; } else { this . contexts [ id ] = context ; } } } param binding registerKeybinding ( binding ) { return this . doRegisterKeybinding ( binding ) ; } param bindings registerKeybindings ( ... bindings ) { return this . doRegisterKeybindings ( bindings , KeybindingScope . DEFAULT ) ; } param binding unregisterKeybinding ( binding ) ; param key unregisterKeybinding ( key ) ; param command unregisterKeybinding ( command ) ; unregisterKeybinding ( arg ) { const keymap = this . keymaps [ KeybindingScope . DEFAULT ] ; const filter = Command . is ( arg ) ? ( { command } ) => command === arg . id : ( { keybinding } ) => Keybinding . is ( arg ) ? keybinding === arg . keybinding : keybinding === arg ; for ( const binding of keymap . filter ( filter ) ) { const idx = keymap . indexOf ( binding ) ; if ( idx !== - 1 ) { keymap . splice ( idx , 1 ) ; } } } protected doRegisterKeybindings ( bindings , scope = KeybindingScope . DEFAULT ) { const toDispose = new DisposableCollection ( ) ; for ( const binding of bindings ) { toDispose . push ( this . doRegisterKeybinding ( binding , scope ) ) ; } return toDispose ; } protected doRegisterKeybinding ( binding , scope = KeybindingScope . DEFAULT ) { try { this . resolveKeybinding ( binding ) ; const scoped = Object . assign ( binding , { scope } ) ; this . insertBindingIntoScope ( scoped , scope ) ; return Disposable . create ( ( ) => { const index = this . keymaps [ scope ] . indexOf ( scoped ) ; if ( index !== - 1 ) { this . keymaps [ scope ] . splice ( index , 1 ) ; } } ) ; } catch ( error ) { this . logger . warn ( `Could not register keybinding:\n  ${ common . Keybinding . stringify ( binding ) }\n${ error }` ) ; return Disposable . NULL ; } } /**
     * Ensures that keybindings are inserted in order of increasing length of binding to ensure that if a
     * user triggers a short keybinding (e.g. ctrl+k), the UI won't wait for a longer one (e.g. ctrl+k enter)
     */ protected insertBindingIntoScope ( item , scope ) { const scopedKeymap = this . keymaps [ scope ] ; const getNumberOfKeystrokes = ( binding ) : number => ( binding . keybinding . trim ( ) . match ( /\s/g ) ?. length ?? 0 ) + 1 ; const numberOfKeystrokesInBinding = getNumberOfKeystrokes ( item ) ; const indexOfFirstItemWithEqualStrokes = scopedKeymap . findIndex ( existingBinding => getNumberOfKeystrokes ( existingBinding ) === numberOfKeystrokesInBinding ) ; if ( indexOfFirstItemWithEqualStrokes > - 1 ) { scopedKeymap . splice ( indexOfFirstItemWithEqualStrokes , 0 , item ) ; } else { scopedKeymap . push ( item ) ; } } /**
     * Ensure that the `resolved` property of the given binding is set by calling the KeyboardLayoutService.
     */ resolveKeybinding ( binding ) { if ( ! binding . resolved ) { const sequence = KeySequence . parse ( binding . keybinding ) ; binding . resolved = sequence . map ( code => this . keyboardLayoutService . resolveKeyCode ( code ) ) ; } return binding . resolved ; } /**
     * Clear all `resolved` properties of registered keybindings so the KeyboardLayoutService is called
     * again to resolve them. This is necessary when the user's keyboard layout has changed.
     */ protected clearResolvedKeybindings ( ) { for ( let i = KeybindingScope . DEFAULT ; i < KeybindingScope . END ; i ++ ) { const bindings = this . keymaps [ i ] ; for ( let j = 0 ; j < bindings . length ; j ++ ) { const binding = bindings [ j ] as ResolvedKeybinding ; binding . resolved = undefined ; } } } /**
     * Checks whether a colliding  common . Keybinding exists in a specific scope.
     *  param binding param scope returns containsKeybindingInScope ( binding , scope = KeybindingScope . USER ) { const bindingKeySequence = this . resolveKeybinding ( binding ) ; const collisions = this . getKeySequenceCollisions ( this . getUsableBindings ( this . keymaps [ scope ] ) , bindingKeySequence ) . filter ( b => b . context === binding . context && ! b . when && ! binding . when ) ; if ( collisions . full . length > 0 ) { return true ; } if ( collisions . partial . length > 0 ) { return true ; } if ( collisions . shadow . length > 0 ) { return true ; } return false ; } /**
     * Get a user visible representation of a  common . Keybinding .
     *  returns an array of strings representing all elements of the  KeySequence defined by the  common . Keybinding param keybinding param separator the separator to be used to stringify  KeyCode s that are part of the  KeySequence acceleratorFor ( keybinding , separator = ' ' ) { const bindingKeySequence = this . resolveKeybinding ( keybinding ) ; return this . acceleratorForSequence ( bindingKeySequence , separator ) ; } /**
     * Get a user visible representation of a  KeySequence .
     *  returns an array of strings representing all elements of the  KeySequence param keySequence param separator the separator to be used to stringify  KeyCode s that are part of the  KeySequence acceleratorForSequence ( keySequence , separator = ' ' ) { return keySequence . map ( keyCode => this . acceleratorForKeyCode ( keyCode , separator ) ) ; } returns a string representing the  KeyCode param keyCode param separator acceleratorForKeyCode ( keyCode , separator = ' ' ) { const keyCodeResult = [ ] ; if ( keyCode . meta && isOSX ) { keyCodeResult . push ( 'Cmd' ) ; } if ( keyCode . ctrl ) { keyCodeResult . push ( 'Ctrl' ) ; } if ( keyCode . alt ) { keyCodeResult . push ( 'Alt' ) ; } if ( keyCode . shift ) { keyCodeResult . push ( 'Shift' ) ; } if ( keyCode . key ) { keyCodeResult . push ( this . acceleratorForKey ( keyCode . key ) ) ; } return keyCodeResult . join ( separator ) ; } /**
     * Return a user visible representation of a single key.
     */ acceleratorForKey ( key ) { if ( isOSX ) { if ( key === Key . ARROW_LEFT ) { return '←' ; } if ( key === Key . ARROW_RIGHT ) { return '→' ; } if ( key === Key . ARROW_UP ) { return '↑' ; } if ( key === Key . ARROW_DOWN ) { return '↓' ; } } const keyString = this . keyboardLayoutService . getKeyboardCharacter ( key ) ; if ( key . keyCode >= Key . KEY_A . keyCode && key . keyCode <= Key . KEY_Z . keyCode || key . keyCode >= Key . F1 . keyCode && key . keyCode <= Key . F24 . keyCode ) { return keyString . toUpperCase ( ) ; } else if ( keyString . length > 1 ) { return keyString . charAt ( 0 ) . toUpperCase ( ) + keyString . slice ( 1 ) ; } else { return keyString ; } } param bindings param candidate protected getKeySequenceCollisions ( bindings , candidate ) { const result = new KeybindingRegistry . KeybindingsResult ( ) ; for ( const binding of bindings ) { try { const bindingKeySequence = this . resolveKeybinding ( binding ) ; const compareResult = KeySequence . compare ( candidate , bindingKeySequence ) ; switch ( compareResult ) { case KeySequence . CompareResult . FULL : { result . full . push ( binding ) ; break ; } case KeySequence . CompareResult . PARTIAL : { result . partial . push ( binding ) ; break ; } case KeySequence . CompareResult . SHADOW : { result . shadow . push ( binding ) ; break ; } } } catch ( error ) { this . logger . warn ( error ) ; } } return result ; } param commandId returns an array of  ScopedKeybinding getKeybindingsForCommand ( commandId ) { const result = [ ] ; for ( let scope = KeybindingScope . END - 1 ; scope >= KeybindingScope . DEFAULT ; scope -- ) { this . keymaps [ scope ] . forEach ( binding => { const command = this . commandRegistry . getCommand ( binding . command ) ; if ( command ) { if ( command . id === commandId ) { result . push ( { ... binding , scope } ) ; } } } ) ; if ( result . length > 0 ) { return result ; } } return result ; } protected isActive ( binding ) { if ( this . isPseudoCommand ( binding . command ) ) { return true ; } const command = this . commandRegistry . getCommand ( binding . command ) ; return ! ! command && ! ! this . commandRegistry . getActiveHandler ( command . id ) ; } param binding param event protected executeKeyBinding ( binding , event ) { if ( this . isPseudoCommand ( binding . command ) ) { } else { const command = this . commandRegistry . getCommand ( binding . command ) ; if ( command ) { if ( this . commandRegistry . isEnabled ( binding . command , binding . args ) ) { this . commandRegistry . executeCommand ( binding . command , binding . args ) . catch ( e => console . error ( 'Failed to execute command:' , e ) ) ; } event . preventDefault ( ) ; event . stopPropagation ( ) ; } } } /**
     * Only execute if it has no context (global context) or if we're in that context.
     */ protected isEnabled ( binding , event ) { const context = binding . context && this . contexts [ binding . context ] ; if ( context && ! context . isEnabled ( binding ) ) { return false ; } if ( binding . when && ! this . whenContextService . match ( binding . when , < HTMLElement > event . target ) ) { return false ; } return true ; } dispatchCommand ( id , target ? ) { const keybindings = this . getKeybindingsForCommand ( id ) ; if ( keybindings . length ) { for ( const keyCode of this . resolveKeybinding ( keybindings [ 0 ] ) ) { this . dispatchKeyDown ( keyCode , target ) ; } } } dispatchKeyDown ( input , target = document . activeElement || window ) { const eventInit = this . asKeyboardEventInit ( input ) ; const emulatedKeyboardEvent = new KeyboardEvent ( 'keydown' , eventInit ) ; target . dispatchEvent ( emulatedKeyboardEvent ) ; } protected asKeyboardEventInit ( input ) { if ( typeof input === 'string' ) { return this . asKeyboardEventInit ( KeyCode . createKeyCode ( input ) ) ; } if ( input instanceof KeyCode ) { return { metaKey : input . meta , shiftKey : input . shift , altKey : input . alt , ctrlKey : input . ctrl , code : input . key && input . key . code , key : ( input && input . character ) || ( input . key && input . key . code ) , keyCode : input . key && input . key . keyCode } ; } return input ; } /**
     * Run the command matching to the given keyboard event.
     */ run ( event ) { if ( event . defaultPrevented ) { return ; } const eventDispatch = this . corePreferences [ 'keyboard.dispatch' ] ; const keyCode = KeyCode . createKeyCode ( event , eventDispatch ) ; if ( keyCode . isModifierOnly ( ) ) { return ; } this . keyboardLayoutService . validateKeyCode ( keyCode ) ; this . keySequence . push ( keyCode ) ; const match = this . matchKeybinding ( this . keySequence , event ) ; if ( match && match . kind === 'partial' ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; this . statusBar . setElement ( 'keybinding-status' , { text : `(${ this . acceleratorForSequence ( this . keySequence , '+' ) }) was pressed, waiting for more keys` , alignment : StatusBarAlignment . LEFT , priority : 2 } ) ; } else { if ( match && match . kind === 'full' ) { this . executeKeyBinding ( match . binding , event ) ; } this . keySequence = [ ] ; this . statusBar . removeElement ( 'keybinding-status' ) ; } } /**
     * Match first binding in the current context.
     * Keybindings ordered by a scope and by a registration order within the scope.
     *
     * FIXME:
     * This method should run very fast since it happens on each keystroke. We should reconsider how keybindings are stored.
     * It should be possible to look up full and partial keybinding for given key sequence for constant time using some kind of tree.
     * Such tree should not contain disabled keybindings and be invalidated whenever the registry is changed.
     */ matchKeybinding ( keySequence , event ? ) { let disabled ; const isEnabled = ( binding ) => { if ( event && ! this . isEnabled ( binding , event ) ) { return false ; } const { command , context , when , keybinding } = binding ; if ( ! this . isUsable ( binding ) ) { disabled = disabled || new Set < string > ( ) ; disabled . add ( JSON . stringify ( { command : command . substr ( 1 ) , context , when , keybinding } ) ) ; return false ; } return ! disabled ?. has ( JSON . stringify ( { command , context , when , keybinding } ) ) ; } ; for ( let scope = KeybindingScope . END ; -- scope >= KeybindingScope . DEFAULT ; ) { for ( const binding of this . keymaps [ scope ] ) { const resolved = this . resolveKeybinding ( binding ) ; const compareResult = KeySequence . compare ( keySequence , resolved ) ; if ( compareResult === KeySequence . CompareResult . FULL && isEnabled ( binding ) ) { return { kind : 'full' , binding } ; } if ( compareResult === KeySequence . CompareResult . PARTIAL && isEnabled ( binding ) ) { return { kind : 'partial' , binding } ; } } } return undefined ; } param binding protected isUsable ( binding ) { return binding . command . charAt ( 0 ) !== '-' ; } param bindings protected getUsableBindings < T extends common . Keybinding > ( bindings ) { return bindings . filter ( binding => this . isUsable ( binding ) ) ; } param commandId isPseudoCommand ( commandId ) { return commandId === KeybindingRegistry . PASSTHROUGH_PSEUDO_COMMAND ; } /**
     * Sets a new keymap replacing all existing  common . Keybinding s in the given scope.
     *  param scope param bindings an array containing the new  common . Keybinding s
      setKeymap ( scope , bindings ) { this . resetKeybindingsForScope ( scope ) ; this . toResetKeymap . set ( scope , this . doRegisterKeybindings ( bindings , scope ) ) ; this . keybindingsChanged . fire ( undefined ) ; } protected readonly toResetKeymap = new Map < KeybindingScope , Disposable > ( ) ; param scope resetKeybindingsForScope ( scope ) { const toReset = this . toResetKeymap . get ( scope ) ; if ( toReset ) { toReset . dispose ( ) ; } } /**
     * Reset keybindings for all scopes(only leaves the default keybindings mapped)
     */ resetKeybindings ( ) { for ( let i = KeybindingScope . DEFAULT + 1 ; i < KeybindingScope . END ; i ++ ) { this . keymaps [ i ] = [ ] ; } } /**
     * Get all  common . Keybinding s for a  KeybindingScope .
     *  returns an array of  common . ScopedKeybinding param scope the keybinding scope to retrieve the  common . Keybinding s for.
      getKeybindingsByScope ( scope ) { return this . keymaps [ scope ] ; } } export namespace KeybindingRegistry { export type Match = { kind : 'full' | 'partial' ; binding : ScopedKeybinding ; } | undefined ; export class KeybindingsResult { full : ScopedKeybinding [ ] = [ ] ; partial : ScopedKeybinding [ ] = [ ] ; shadow : ScopedKeybinding [ ] = [ ] ; param other return merge ( other ) { this . full . push ( ... other . full ) ; this . partial . push ( ... other . partial ) ; this . shadow . push ( ... other . shadow ) ; return this ; } param fn return filter ( fn ) { const result = new KeybindingsResult ( ) ; result . full = this . full . filter ( fn ) ; result . partial = this . partial . filter ( fn ) ; result . shadow = this . shadow . filter ( fn ) ; return result ; } } }