/**
 * # Converting Keybinding Definitions to Actions
 *
 * This module defines the schema of the configuration file using TypeScript
 * interfaces. We parse the configuration JSON to TypeScript objects which
 * directly define all the valid keyboard sequences and the commands that these
 * will invoke.
 */ import * as vscode from 'vscode' ; /**
 * ## Action Definitions
 *
 * The keybinding configuration consist of _actions_ that can take three forms:
 * an action can be a command (defined later), a keymap, or a number that refers
 * to a keymap defined earlier.
 */ export type Action = Command | Keymap | number ; /**
 * Commands can be invoked in four ways: by specifying just command a name
 * (string), or using a conditional command, a command with parameters, or a
 * sequence (array) of commands. The definition is recursive, meaning that a
 * sequence can contain all four types of commands.
 */ export type Command = string | Conditional | Parameterized | Command [ ] ; /**
 * A conditional command consist of condition (a JavaScript expression) and set
 * of branches to take depending on the result of the condition. Each branch can
 * be any type of command defined above.
 */ export interface Conditional { condition : string ; [ branch ] : Command ; } /**
 * A command that takes arguments can be specified using the `Parameterized`
 * interface. Arguments can be given either as an object or a string, which
 * is assumed to contain a valid JS expression. Additionally, you can specify
 * that the command is run multiple times by setting the `repeat` property. The
 * property must be either a number, or a JS expression that evaluates to a
 * number. If it evaluates to some other type, the expression is used as a
 * condition that is evaluated after the command is run. If the expression
 * returns a truthy value, the command is repeated.
 */ export interface Parameterized { command : string ; args ? : { } | string ; repeat ? : number | string ; } /**
 * A keymap is a dictionary of keys (characters) to actions. Keys are either
 * single characters or character ranges, denoted by sequences of `<char>,<char>`
 * and `<char>-<char>`. Values of the dictionary can be also nested keymaps.
 * This is how you can define commands that require  multiple keypresses.
 *
 * ![keymap example](../images/keymap.png)
 * When the value of a key is number, it refers to another keymap whose `id`
 * equals the number. The number can also point to the same  keymap where it
 * resides. With this mechanism, you can define _recursive_ keymaps that can
 * take (theoretically) infinitely long key sequences. The picture on the right
 * illustrates this.
 *
 * The `help` field contains help text that is shown in the status bar when the
 * keymap is active.
 */ export interface Keymap { id : number ; help : string ; [ key ] : Action ; } /**
 * ## Cursor Shapes
 *
 * You can use various cursor shapes in different modes. The list of available
 * shapes is defined below.
 */ type Cursor = "block" | "block-outline" | "line" | "line-thin" | "underline" | "underline-thin" | undefined ; /**
 * ## Configuration State
 *
 * The variables below contain the current cursor configuration.
 */ let insertCursorStyle ; let normalCursorStyle ; let searchCursorStyle ; let selectCursorStyle ; let insertStatusText ; let normalStatusText ; let searchStatusText ; let selectStatusText ; let insertStatusColor ; let normalStatusColor ; let searchStatusColor ; let selectStatusColor ; /**
 * Another thing you can set in config, is whether ModalEdit starts in normal
 * mode.
 */ let startInNormalMode ; /**
 * The root of the action configuration is keymap. This defines what key
 * sequences will be run when keys are pressed in normal mode.
 */ let baseKeymap ; let selectKeymap ; /**
 * The current active keymap is stored here. The active keymap changes when the
 * user invokes a multi-key action sequence.
 */ let currentKeymap = null ; /**
 * The last command run is also stored. This is needed to run commands which
 * capture the keyboard.
 */ let lastCommand ; /**
 * The key sequence that user has pressed is stored for error reporting
 * purposes and to make it available to command arguments. Since commands
 * can invoke other commands through `typeNormalKeys` command, we need to
 * maintain the key sequences in a stack. Both current key sequence and the
 * stack is initialized to empty array.
 */ let keySequence = [ ] ; let keySeqStack = [ ] ; /**
 * We need a dictionary that returns a keymap for given id.
 */ let keymapsById ; /**
 * ## Configuration Accessors
 *
 * The following functions return the current configuration settings.
 */ export function getInsertStyles ( ) { return [ insertCursorStyle , insertStatusText , insertStatusColor ] ; } export function getNormalStyles ( ) { return [ normalCursorStyle , normalStatusText , normalStatusColor ] ; } export function getSearchStyles ( ) { return [ searchCursorStyle , searchStatusText , searchStatusColor ] ; } export function getSelectStyles ( ) { return [ selectCursorStyle , selectStatusText , selectStatusColor ] ; } export function getStartInNormalMode ( ) { return startInNormalMode ; } /**
 * You can also set the last command from outside the module.
 */ export function setLastCommand ( command ) { lastCommand = command ; } /**
 * ## Logging
 *
 * To enable logging and error reporting ModalEdit creates an output channel
 * that is visible in the output pane. The channel is created in the extension
 * activation hook, but it is passed to this module using the `setOutputChannel`
 * function.
 */ let outputChannel ; export function setOutputChannel ( channel ) { outputChannel = channel ; } /**
 * Once the channel is set, we can output messages to it using the `log`
 * function.
 */ export function log ( message ) { outputChannel . appendLine ( message ) ; } /**
 * ## Updating Configuration from settings.json
 *
 * Whenever you save the user-level `settings.json` or the one located in the
 * `.vsode` directory VS Code calls this function that updates the current
 * configuration.
 */ export function updateFromConfig ( ) { const config = vscode . workspace . getConfiguration ( "modaledit" ) ; UpdateKeybindings ( config ) ; insertCursorStyle = toVSCursorStyle ( config . get < Cursor > ( "insertCursorStyle" , "line" ) ) ; normalCursorStyle = toVSCursorStyle ( config . get < Cursor > ( "normalCursorStyle" , "block" ) ) ; searchCursorStyle = toVSCursorStyle ( config . get < Cursor > ( "searchCursorStyle" , "underline" ) ) ; selectCursorStyle = toVSCursorStyle ( config . get < Cursor > ( "selectCursorStyle" , "line-thin" ) ) ; insertStatusText = config . get ( "insertStatusText" , "-- $(edit) INSERT --" ) ; normalStatusText = config . get ( "normalStatusText" , "-- $(move) NORMAL --" ) ; searchStatusText = config . get ( "searchStatusText" , "$(search) SEARCH" ) ; selectStatusText = config . get ( "selectStatusText" , "-- $(paintcan) VISUAL --" ) ; insertStatusColor = config . get ( "insertStatusColor" ) || undefined ; normalStatusColor = config . get ( "normalStatusColor" ) || undefined ; searchStatusColor = config . get ( "searchStatusColor" ) || undefined ; selectStatusColor = config . get ( "selectStatusColor" ) || undefined ; startInNormalMode = config . get < boolean > ( "startInNormalMode" , true ) ; } /**
 * The following function updates base keymap and select-mode keymap.
 */ function UpdateKeybindings ( config ) { log ( "Validating keybindings in 'settings.json'..." ) ; keymapsById = { } ; errors = 0 ; let sel = config . get < object > ( "selectbindings" ) ; if ( isKeymap ( sel ) ) { selectKeymap = sel ; validateAndResolveKeymaps ( sel ) ; } let base = config . get < object > ( "keybindings" ) ; if ( isKeymap ( base ) ) { baseKeymap = base ; validateAndResolveKeymaps ( base ) ; } else log ( "ERROR: Invalid configuration structure. Keybindings not updated." ) ; if ( errors > 0 ) log ( `Found ${ errors } error${ errors > 1 ? "s" : "" }. ` + "Keybindings might not work correctly." ) ; else log ( "Validation completed successfully." ) ; } /**
 * To make sure that the keybinding section is valid, we define a function that
 * checks it. At the same time the function resolves all the keymaps that are
 * referred by an id. It records the number of errors.
 */ let errors ; /**
 * The keymap ranges are recognized with the following regular expression.
 * Examples of valid key sequences include:
 *
 * - `0-9`
 * - `a,b,c`
 * - `d,e-h,l`
 *
 * Basically you can add individual characters to the range with a comma `,` and
 * an ASCII range with dash `-`. The ASCII code of the first character must be
 * smaller than the second one's.
 */ let keyRE = /^.([\-,].)+$/ ; /**
 * The function itself is recursive; it calls itself, if it finds a nested
 * keymap. It stores all the keymaps it encounters in the `keymapsById`
 * dictionary.
 */ function validateAndResolveKeymaps ( keybindings ) { function error ( message ) { log ( "ERROR: " + message ) ; errors ++ ; } if ( typeof keybindings . id === 'number' ) keymapsById [ keybindings . id ] = keybindings ; for ( let key in keybindings ) { if ( keybindings . hasOwnProperty ( key ) && key != "id" && key != "help" ) { let target = keybindings [ key ] ; if ( isKeymap ( target ) ) validateAndResolveKeymaps ( target ) ; else if ( typeof target === 'number' ) { let id = target ; target = keymapsById [ id ] ; if ( ! target ) error ( `Undefined keymap id: ${ id }` ) ; else keybindings [ key ] = target ; } if ( key . match ( keyRE ) ) for ( let i = 1 ; i < key . length ; i += 2 ) { if ( key [ i ] == '-' ) { let first = key . charCodeAt ( i - 1 ) ; let last = key . charCodeAt ( i + 1 ) ; if ( first > last ) error ( `Invalid key range: "${ key }"` ) ; else for ( let i = first ; i <= last ; i ++ ) keybindings [ String . fromCharCode ( i ) ] = target ; } else { keybindings [ key [ i - 1 ] ] = target ; keybindings [ key [ i + 1 ] ] = target ; } } else if ( key . length != 1 ) error ( `Invalid key binding: "${ key }"` ) ; } } } /**
 * The helper function below converts cursor styles specified in configuration
 * to enumeration members used by VS Code.
 */ function toVSCursorStyle ( cursor ) { switch ( cursor ) { case "line" : return vscode . TextEditorCursorStyle . Line ; case "block" : return vscode . TextEditorCursorStyle . Block ; case "underline" : return vscode . TextEditorCursorStyle . Underline ; case "line-thin" : return vscode . TextEditorCursorStyle . LineThin ; case "block-outline" : return vscode . TextEditorCursorStyle . BlockOutline ; case "underline-thin" : return vscode . TextEditorCursorStyle . UnderlineThin ; default : return vscode . TextEditorCursorStyle . Line ; } } /**
 * ## Type Predicates
 *
 * Since JavaScript does not have dynamic type information we need to write
 * functions that check which type of action we get from the configuration.
 * First we define a high-level type predicate that checks if a value is
 * an action.
 */ function isAction ( x ) { return isCommand ( x ) || isKeymap ( x ) || isNumber ( x ) ; } /**
 * This one checks whether a value is a string.
 */ function isString ( x ) { return x && typeof x === "string" ; } /**
 * This one checks whether a value is a number.
 */ function isNumber ( x ) { return x && typeof x === "number" ; } /**
 * This one identifies an object.
 */ function isObject ( x ) { return x && typeof x === "object" ; } /**
 * This checks if a value is a command.
 */ function isCommand ( x ) { return isString ( x ) || isParameterized ( x ) || isConditional ( x ) || isCommandSequence ( x ) ; } /**
 * This checks if a value is an array of commands.
 */ function isCommandSequence ( x ) { return Array . isArray ( x ) && x . every ( isCommand ) ; } /**
 * This recognizes a conditional action.
 */ function isConditional ( x ) { return isObject ( x ) && isString ( x . condition ) && Object . keys ( x ) . every ( key => key === "condition" || isCommand ( x [ key ] ) ) ; } /**
 * This asserts that a value is a parameterized command.
 */ function isParameterized ( x ) { return isObject ( x ) && isString ( x . command ) && ( ! x . args || isObject ( x . args ) || isString ( x . args ) ) && ( ! x . repeat || isNumber ( x . repeat ) || isString ( x . repeat ) ) ; } /**
 * And finally this one checks if a value is a keymap.
 */ function isKeymap ( x ) { return isObject ( x ) && ! isConditional ( x ) && ! isParameterized ( x ) && Object . values ( x ) . every ( isAction ) ; } /**
 * ## Executing Commands
 *
 * In the end all keybindings will invoke one or more VS Code commands. The
 * following function runs a command whose name and arguments are given as
 * parameters. If the command throws an exception because of invalid arguments,
 * for example, the error is shown in the popup window at the corner of the
 * screen.
 */ async function executeVSCommand ( command , ... rest ) { try { await vscode . commands . executeCommand ( command , ... rest ) ; lastCommand = command ; } catch ( error ) { vscode . window . showErrorMessage ( error . message ) ; } } /**
 * `evalString` function evaluates JavaScript expressions. Before doing so, it
 * defines some variables that can be used in the evaluated text.
 */ function evalString ( str , __selecting ) { let __file = undefined ; let __line = undefined ; let __col = undefined ; let __char = undefined ; let __selection = undefined ; let __keySequence = keySequence ; let __keys = keySequence ; let __rkeys = keySequence . slice ( ) . reverse ( ) ; let __cmd = __keys . join ( '' ) ; let __rcmd = __rkeys . join ( '' ) ; let editor = vscode . window . activeTextEditor ; if ( editor ) { let cursor = editor . selection . active ; __file = editor . document . fileName ; __line = cursor . line ; __col = cursor . character ; __char = editor . document . getText ( new vscode . Range ( cursor , cursor . translate ( { characterDelta : 1 } ) ) ) ; __selection = editor . document . getText ( editor . selection ) ; } try { return eval ( `(${ str })` ) ; } catch ( error ) { vscode . window . showErrorMessage ( "Evaluation error: " + error . message ) ; } } /**
 * We need the evaluation function when executing conditional command. The
 * condition is evaluated and if a key is found that matches the result, it is
 * executed.
 */ async function executeConditional ( cond , selecting ) { let res = evalString ( cond . condition , selecting ) ; let branch = isString ( res ) ? res : JSON . stringify ( res ) ; if ( branch && isAction ( cond [ branch ] ) ) await execute ( cond [ branch ] , selecting ) ; } /**
 * Parameterized commands can get their arguments in two forms: as a string
 * that is evaluated to get the actual arguments, or as an object. Before
 * executing the command, we inspect the `repeat` property. If it is string
 * we evaluate it, and check if the result is a number. If so, we update the
 * `repeat` variable that designates repetition count. If not, we treate it as
 * a continue condition. The subroutine `exec` runs the command either `repeat`
 * times or as long as the expression in the `repeat` property returns a truthy
 * value.
 */ async function executeParameterized ( action , selecting ) { let repeat = 1 ; async function exec ( args ? ) { let cont = true ; if ( isString ( repeat ) ) do { await executeVSCommand ( action . command , args ) ; cont = evalString ( repeat , selecting ) ; } while ( cont ) ; else for ( let i = 0 ; i < repeat ; i ++ ) await executeVSCommand ( action . command , args ) ; } if ( action . repeat ) { if ( isString ( action . repeat ) ) { let val = evalString ( action . repeat , selecting ) ; if ( typeof val === 'number' ) repeat = Math . max ( 1 , val ) ; else repeat = action . repeat ; } else repeat = Math . max ( 1 , action . repeat ) ; } if ( action . args ) { if ( typeof action . args === 'string' ) await exec ( evalString ( action . args , selecting ) ) ; else await exec ( action . args ) ; } else await exec ( ) ; } /**
 * ## Executing Actions
 *
 * Before running any commands, we need to identify which type of action we got.
 * Depending on the type we use different function to execute the command. If
 * the action is not a command, it has to be a keymap. Since we resolved `id`
 * referenences in `validateAndResolveKeymaps`, an action has to be a keymap
 * object at this point. We set the new keymap as the active one.
 */ async function execute ( action , selecting ) { currentKeymap = null ; if ( isString ( action ) ) await executeVSCommand ( action ) ; else if ( isCommandSequence ( action ) ) for ( const command of action ) await execute ( command , selecting ) ; else if ( isConditional ( action ) ) await executeConditional ( action , selecting ) ; else if ( isParameterized ( action ) ) await executeParameterized ( action , selecting ) ; else currentKeymap = < Keymap > action ; } /**
 * ## Key Press Handler
 *
 * Now that the plumbing of actions is implemented, it is straightforward to
 * map the pressed key to an action. The special case occurs when a command
 * captures the keyboard. Then we rerun the previous command and give the key
 * to it as an argument.
 *
 * Otherwise we just check if the current keymap contains binding for the key
 * pressed, and execute the action. If not, we present an error to the user.
 *
 * As a last step the function returns `true`, if the current keymap is `null`.
 * This indicates that the key invoked a command instead of just changing the
 * active keymap.
 */ export async function handleKey ( key , selecting , capture ) { function error ( ) { vscode . window . showWarningMessage ( "ModalEdit: Undefined key binding: " + keySequence . join ( " - " ) ) ; currentKeymap = null ; } if ( ! currentKeymap ) { keySeqStack . push ( keySequence ) ; keySequence = [ ] ; } keySequence . push ( key ) ; if ( capture && lastCommand ) await executeVSCommand ( lastCommand , key ) ; else if ( currentKeymap ) { if ( currentKeymap [ key ] ) await execute ( currentKeymap [ key ] , selecting ) ; else error ( ) ; } else { if ( selecting && selectKeymap [ key ] ) await execute ( selectKeymap [ key ] , selecting ) ; else if ( baseKeymap [ key ] ) await execute ( baseKeymap [ key ] , selecting ) ; else error ( ) ; } if ( ! currentKeymap ) keySequence = keySeqStack . pop ( ) ! ; return ! currentKeymap ; } /**
 * ## Keymap Help
 *
 * When defining complex key sequences you can help the user by defining what
 * keys she can press next and what they do. If the help is defined, it is shown
 * in the status bar.
 */ export function getHelp ( ) { return currentKeymap ?. help ; }