import NeovimStore from './store' ; import { compositionStart , compositionEnd , inputToNeovim , notifyFocusChanged } from './actions' ; import log from '../log' ; const OnDarwin = global . process . platform === 'darwin' ; const IsAlpha = /^[a-zA-Z]$/ ; export default class NeovimInput { element : HTMLInputElement ; fake_element : HTMLSpanElement ; ime_running : boolean ; static shouldIgnoreOnKeydown ( event ) { const { ctrlKey , shiftKey , altKey , keyCode , metaKey } = event ; return ( ( ! ctrlKey && ! altKey && ! metaKey ) || ( shiftKey && keyCode === 16 ) || ( ctrlKey && keyCode === 17 ) || ( altKey && keyCode === 18 ) || ( metaKey && keyCode === 91 ) ) ; } static getVimSpecialCharFromKeyCode ( key_code , shift ) { switch ( key_code ) { case 0 : return 'Nul' ; case 8 : return 'BS' ; case 9 : return 'Tab' ; case 10 : return 'NL' ; case 13 : return 'CR' ; case 33 : return 'PageUp' ; case 34 : return 'PageDown' ; case 27 : return 'Esc' ; case 32 : return 'Space' ; case 35 : return 'End' ; case 36 : return 'Home' ; case 37 : return 'Left' ; case 38 : return 'Up' ; case 39 : return 'Right' ; case 40 : return 'Down' ; case 45 : return 'Insert' ; case 46 : return 'Del' ; case 47 : return 'Help' ; case 92 : return 'Bslash' ; case 112 : return 'F1' ; case 113 : return 'F2' ; case 114 : return 'F3' ; case 115 : return 'F4' ; case 116 : return 'F5' ; case 117 : return 'F6' ; case 118 : return 'F7' ; case 119 : return 'F8' ; case 120 : return 'F9' ; case 121 : return 'F10' ; case 122 : return 'F11' ; case 123 : return 'F12' ; case 124 : return 'Bar' ; case 127 : return 'Del' ; case 188 : return shift ? 'LT' : null ; default : return null ; } } static getVimSpecialCharFromKey ( event ) { const key = event . key ; if ( key . length === 1 ) { switch ( key ) { case '<' : return event . ctrlKey || event . altKey ? 'LT' : null ; case ' ' : return 'Space' ; case '\0' : return 'Nul' ; default : return null ; } } if ( key [ 0 ] === 'F' ) { return /^F\d+/ . test ( key ) ? key : null ; } const ctrl = event . ctrlKey ; const key_code = event . keyCode ; switch ( key ) { case 'Escape' : { if ( ctrl && key_code !== 27 ) { return '[' ; } else { return 'Esc' ; } } case 'Backspace' : { if ( ctrl && key_code === 72 ) { return 'h' ; } else { return 'BS' ; } } case 'Tab' : { if ( ctrl && key_code === 73 ) { return 'i' ; } else { return 'Tab' ; } } case 'Enter' : { if ( ctrl && key_code === 77 ) { return 'm' ; } else if ( ctrl && key_code === 67 ) { return 'c' ; } else { return 'CR' ; } } case 'PageUp' : return 'PageUp' ; case 'PageDown' : return 'PageDown' ; case 'End' : return 'End' ; case 'Home' : return 'Home' ; case 'ArrowLeft' : return 'Left' ; case 'ArrowUp' : return 'Up' ; case 'ArrowRight' : return 'Right' ; case 'ArrowDown' : return 'Down' ; case 'Insert' : return 'Insert' ; case 'Delete' : return 'Del' ; case 'Help' : return 'Help' ; case 'Unidentified' : return null ; default : return null ; } } static getVimSpecialCharInput ( event ) { const should_fallback = event . key === undefined || ( event . key === '\0' && event . keyCode !== 0 ) ; const special_char = should_fallback ? NeovimInput . getVimSpecialCharFromKeyCode ( event . keyCode , event . shiftKey ) : NeovimInput . getVimSpecialCharFromKey ( event ) ; if ( ! special_char ) { return null ; } let vim_input = '<' ; if ( event . ctrlKey ) { vim_input += 'C-' ; } if ( event . metaKey ) { vim_input += 'D-' ; } if ( event . altKey ) { vim_input += 'A-' ; } if ( event . shiftKey && special_char !== 'LT' ) { vim_input += 'S-' ; } vim_input += special_char + '>' ; return vim_input ; } static replaceKeyToAvoidCtrlShiftSpecial ( ctrl , shift , key ) { if ( ! ctrl || shift ) { return key ; } switch ( key ) { case '6' : return '^' ; case '-' : return '_' ; case '2' : return '@' ; default : return key ; } } static getVimInputFromKeyCode ( event ) { let modifiers = '' ; if ( event . ctrlKey ) { modifiers += 'C-' ; } if ( event . metaKey ) { modifiers += 'D-' ; } if ( event . altKey ) { modifiers += 'A-' ; } if ( event . shiftKey ) { modifiers += 'S-' ; } let vim_input = NeovimInput . replaceKeyToAvoidCtrlShiftSpecial ( event . ctrlKey , event . shiftKey , String . fromCharCode ( event . keyCode ) . toLowerCase ( ) ) ; if ( modifiers !== '' ) { vim_input = `<${ modifiers }${ vim_input }>` ; } return vim_input ; } constructor ( private readonly store ) { this . ime_running = false ; this . element = this . store . dom . input as HTMLInputElement ; this . element . addEventListener ( 'compositionstart' , this . startComposition . bind ( this ) ) ; this . element . addEventListener ( 'compositionend' , this . endComposition . bind ( this ) ) ; this . element . addEventListener ( 'keydown' , this . onInputNonText . bind ( this ) ) ; this . element . addEventListener ( 'input' , this . onInputText . bind ( this ) ) ; this . element . addEventListener ( 'blur' , this . onBlur . bind ( this ) ) ; this . element . addEventListener ( 'focus' , this . onFocus . bind ( this ) ) ; this . store . on ( 'cursor' , this . updateElementPos . bind ( this ) ) ; this . store . on ( 'font-size-changed' , this . updateFontSize . bind ( this ) ) ; this . fake_element = this . store . dom . preedit as HTMLSpanElement ; const { face } = this . store . font_attr ; this . element . style . fontFamily = face ; this . fake_element . style . fontFamily = face ; this . updateFontSize ( ) ; this . focus ( ) ; } startComposition ( _ ) { log . debug ( 'start composition' ) ; this . element . style . color = this . store . fg_color ; this . element . style . backgroundColor = this . store . bg_color ; this . element . style . width = 'auto' ; this . store . dispatcher . dispatch ( compositionStart ( ) ) ; this . ime_running = true ; } endComposition ( event ) { log . debug ( 'end composition' ) ; this . inputToNeovim ( event . data , event ) ; this . element . style . color = 'transparent' ; this . element . style . backgroundColor = 'transparent' ; this . element . style . width = '1px' ; this . store . dispatcher . dispatch ( compositionEnd ( ) ) ; this . ime_running = false ; } focus ( ) { this . element . focus ( ) ; } onFocus ( ) { this . store . dispatcher . dispatch ( notifyFocusChanged ( true ) ) ; this . store . dispatcher . dispatch ( inputToNeovim ( '<FocusGained>' ) ) ; } onBlur ( e ) { e . preventDefault ( ) ; this . store . dispatcher . dispatch ( notifyFocusChanged ( false ) ) ; this . store . dispatcher . dispatch ( inputToNeovim ( '<FocusLost>' ) ) ; } onInputNonText ( event ) { log . debug ( 'Keydown event:' , event ) ; if ( this . ime_running ) { log . debug ( 'IME is running.  Input canceled.' ) ; return ; } const special_sequence = NeovimInput . getVimSpecialCharInput ( event ) ; if ( special_sequence ) { this . inputToNeovim ( special_sequence , event ) ; return ; } if ( NeovimInput . shouldIgnoreOnKeydown ( event ) ) { return ; } const should_osx_workaround = OnDarwin && event . altKey && ! event . ctrlKey && this . store . mode === 'normal' ; if ( this . store . alt_key_disabled && event . altKey ) { Object . defineProperty ( event , 'altKey' , { value : false } ) ; } if ( this . store . meta_key_disabled && event . metaKey ) { return ; } if ( should_osx_workaround ) { this . inputToNeovim ( NeovimInput . getVimInputFromKeyCode ( event ) , event ) ; return ; } if ( event . key ) { if ( event . key . length === 1 ) { let input = '<' ; if ( event . ctrlKey ) { input += 'C-' ; } if ( event . metaKey ) { input += 'D-' ; } if ( event . altKey ) { input += 'A-' ; } if ( event . shiftKey && IsAlpha . test ( event . key ) ) { input += 'S-' ; } if ( input === '<' ) { this . inputToNeovim ( event . key , event ) ; } else { const key = NeovimInput . replaceKeyToAvoidCtrlShiftSpecial ( event . ctrlKey , event . shiftKey , event . key ) ; this . inputToNeovim ( input + key + '>' , event ) ; } } else { log . warn ( "Invalid key input on 'keydown': " , event . key ) ; } } else { this . inputToNeovim ( NeovimInput . getVimInputFromKeyCode ( event ) , event ) ; } } inputToNeovim ( input , event ) { this . store . dispatcher . dispatch ( inputToNeovim ( input ) ) ; log . debug ( 'Input to neovim:' , JSON . stringify ( input ) ) ; event . preventDefault ( ) ; event . stopPropagation ( ) ; const t = event . target as HTMLInputElement ; if ( t . value ) { t . value = '' ; } } onInputText ( event ) { log . debug ( 'Input event:' , event ) ; if ( this . ime_running ) { log . debug ( 'IME is running.  Input canceled.' ) ; this . fake_element . innerText = this . element . value ; this . fake_element . style . display = 'inline' ; const width = this . fake_element . getBoundingClientRect ( ) . width ; this . fake_element . style . display = 'none' ; this . element . style . width = width + 'px' ; return ; } const t = event . target as HTMLInputElement ; if ( t . value === '' ) { log . warn ( 'onInputText: Empty' ) ; return ; } const input = t . value !== '<' ? t . value : '<LT>' ; this . inputToNeovim ( input , event ) ; } updateElementPos ( ) { const { line , col } = this . store . cursor ; const { width , height } = this . store . font_attr ; const x = col * width ; const y = line * height ; this . element . style . left = x + 'px' ; this . element . style . top = y + 'px' ; } updateFontSize ( ) { const { specified_px : font_size } = this . store . font_attr ; this . element . style . fontSize = font_size + 'px' ; this . fake_element . style . fontSize = font_size + 'px' ; } }