diff --git a/extension.ts b/extension.ts index 78544f627c6..793615e3fa5 100644 --- a/extension.ts +++ b/extension.ts @@ -68,20 +68,19 @@ export async function getAndUpdateModeHandler(forceSyncAndUpdate = false): Promi } async function loadConfiguration() { - const configurationErrors = await configuration.load(); const logger = Logger.get('Configuration'); - const numErrors = configurationErrors.filter(e => e.level === 'error').length; - logger.debug(`${numErrors} errors found with vim configuration`); + const validatorResults = await configuration.load(); + logger.debug(`${validatorResults.numErrors()} errors found with vim configuration`); - if (numErrors > 0) { - for (let configurationError of configurationErrors) { - switch (configurationError.level) { + if (validatorResults.numErrors() > 0) { + for (let validatorResult of validatorResults.get()) { + switch (validatorResult.level) { case 'error': - logger.error(configurationError.message); + logger.error(validatorResult.message); break; case 'warning': - logger.warn(configurationError.message); + logger.warn(validatorResult.message); break; } } diff --git a/src/actions/plugins/imswitcher.ts b/src/actions/plugins/imswitcher.ts index 58f59d13788..3808cbba57b 100644 --- a/src/actions/plugins/imswitcher.ts +++ b/src/actions/plugins/imswitcher.ts @@ -1,31 +1,24 @@ import * as util from '../../util/util'; -import { Globals } from '../../globals'; +import { Logger } from '../../util/logger'; import { ModeName } from '../../mode/mode'; import { configuration } from '../../configuration/configuration'; -import { exists } from 'fs'; -import { Logger } from '../../util/logger'; -import { promisify } from 'util'; /** * InputMethodSwitcher changes input method when mode changed */ export class InputMethodSwitcher { + private readonly logger = Logger.get('IMSwitcher'); + private execute: (cmd: string) => Promise; + private savedIMKey = ''; + constructor(execute: (cmd: string) => Promise = util.executeShell) { this.execute = execute; } - private execute: (cmd: string) => Promise; - private readonly logger = Logger.get('IMSwitcher'); - private savedIMKey = ''; - public async switchInputMethod(prevMode: ModeName, newMode: ModeName) { if (configuration.autoSwitchInputMethod.enable !== true) { return; } - if (!this.isConfigurationValid()) { - this.disableIMSwitch(); - return; - } // when you exit from insert-like mode, save origin input method and set it to default let isPrevModeInsertLike = this.isInsertLikeMode(prevMode); let isNewModeInsertLike = this.isInsertLikeMode(newMode); @@ -41,21 +34,13 @@ export class InputMethodSwitcher { // save origin input method and set input method to default private async switchToDefaultIM() { const obtainIMCmd = configuration.autoSwitchInputMethod.obtainIMCmd; - const rawObtainIMCmd = this.getRawCmd(obtainIMCmd); - if ((await promisify(exists)(rawObtainIMCmd)) || Globals.isTesting) { - try { - const insertIMKey = await this.execute(obtainIMCmd); - if (insertIMKey !== undefined) { - this.savedIMKey = insertIMKey.trim(); - } - } catch (e) { - this.logger.error(`Error switching to default IM. err=${e}`); + try { + const insertIMKey = await this.execute(obtainIMCmd); + if (insertIMKey !== undefined) { + this.savedIMKey = insertIMKey.trim(); } - } else { - this.logger.error( - `Unable to find ${rawObtainIMCmd}. Check your 'vim.autoSwitchInputMethod.obtainIMCmd' in VSCode setting.` - ); - this.disableIMSwitch(); + } catch (e) { + this.logger.error(`Error switching to default IM. err=${e}`); } const defaultIMKey = configuration.autoSwitchInputMethod.defaultIM; @@ -73,21 +58,13 @@ export class InputMethodSwitcher { private async switchToIM(imKey: string) { let switchIMCmd = configuration.autoSwitchInputMethod.switchIMCmd; - const rawSwitchIMCmd = this.getRawCmd(switchIMCmd); - if ((await promisify(exists)(rawSwitchIMCmd)) || Globals.isTesting) { - if (imKey !== '' && imKey !== undefined) { - switchIMCmd = switchIMCmd.replace('{im}', imKey); - try { - await this.execute(switchIMCmd); - } catch (e) { - this.logger.error(`Error switching to IM. err=${e}`); - } + if (imKey !== '' && imKey !== undefined) { + switchIMCmd = switchIMCmd.replace('{im}', imKey); + try { + await this.execute(switchIMCmd); + } catch (e) { + this.logger.error(`Error switching to IM. err=${e}`); } - } else { - this.logger.error( - `Unable to find ${rawSwitchIMCmd}. Check your 'vim.autoSwitchInputMethod.switchIMCmd' in VSCode setting.` - ); - this.disableIMSwitch(); } } @@ -99,35 +76,4 @@ export class InputMethodSwitcher { ]); return insertLikeModes.has(mode); } - - private getRawCmd(cmd: string): string { - return cmd.split(' ')[0]; - } - - private disableIMSwitch() { - this.logger.warn('disabling IMSwitch'); - configuration.autoSwitchInputMethod.enable = false; - } - - private isConfigurationValid(): boolean { - let switchIMCmd = configuration.autoSwitchInputMethod.switchIMCmd; - if (!switchIMCmd.includes('{im}')) { - this.logger.error( - 'vim.autoSwitchInputMethod.switchIMCmd is incorrect, \ - it should contain the placeholder {im}' - ); - return false; - } - let obtainIMCmd = configuration.autoSwitchInputMethod.obtainIMCmd; - if (obtainIMCmd === undefined || obtainIMCmd === '') { - this.logger.error('vim.autoSwitchInputMethod.obtainIMCmd is empty'); - return false; - } - let defaultIMKey = configuration.autoSwitchInputMethod.defaultIM; - if (defaultIMKey === undefined || defaultIMKey === '') { - this.logger.error('vim.autoSwitchInputMethod.defaultIM is empty'); - return false; - } - return true; - } } diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index b777e1719c8..3e66dc5be95 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; -import { ConfigurationError } from './configurationError'; import { Globals } from '../globals'; import { Notation } from './notation'; +import { ValidatorResults } from './iconfigurationValidator'; import { VsCodeContext } from '../util/vscode-context'; import { configurationValidator } from './configurationValidator'; import { @@ -66,13 +66,11 @@ class Configuration implements IConfiguration { 'underline-thin': vscode.TextEditorCursorStyle.UnderlineThin, }; - public async load(): Promise { + public async load(): Promise { let vimConfigs: any = Globals.isTesting ? Globals.mockConfiguration : this.getConfiguration('vim'); - let configurationErrors = new Array(); - /* tslint:disable:forin */ // Disable forin rule here as we make accessors enumerable.` for (const option in this) { @@ -87,72 +85,7 @@ class Configuration implements IConfiguration { this.leader = Notation.NormalizeKey(this.leader, this.leaderDefault); - // remapped keys - const modeKeyBindingsKeys = [ - 'insertModeKeyBindings', - 'insertModeKeyBindingsNonRecursive', - 'normalModeKeyBindings', - 'normalModeKeyBindingsNonRecursive', - 'visualModeKeyBindings', - 'visualModeKeyBindingsNonRecursive', - ]; - for (const modeKeyBindingsKey of modeKeyBindingsKeys) { - let keybindings = configuration[modeKeyBindingsKey]; - - const modeKeyBindingsMap = new Map(); - for (let i = keybindings.length - 1; i >= 0; i--) { - let remapping = keybindings[i] as IKeyRemapping; - - // validate - let remappingErrors = await configurationValidator.isRemappingValid(remapping); - configurationErrors = configurationErrors.concat(remappingErrors); - - if (remappingErrors.filter(e => e.level === 'error').length > 0) { - // errors with remapping, skip - keybindings.splice(i, 1); - continue; - } - - // normalize - if (remapping.before) { - remapping.before.forEach( - (key, idx) => (remapping.before[idx] = Notation.NormalizeKey(key, this.leader)) - ); - } - - if (remapping.after) { - remapping.after.forEach( - (key, idx) => (remapping.after![idx] = Notation.NormalizeKey(key, this.leader)) - ); - } - - // check for duplicates - const beforeKeys = remapping.before.join(''); - if (modeKeyBindingsMap.has(beforeKeys)) { - configurationErrors.push({ - level: 'error', - message: `${remapping.before}. Duplicate remapped key for ${beforeKeys}.`, - }); - continue; - } - - // add to map - modeKeyBindingsMap.set(beforeKeys, remapping); - } - - configuration[modeKeyBindingsKey + 'Map'] = modeKeyBindingsMap; - } - - // neovim - let neovimErrors = await configurationValidator.isNeovimValid( - configuration.enableNeovim, - configuration.neovimPath - ); - configurationErrors = configurationErrors.concat(neovimErrors); - if (neovimErrors.filter(e => e.level === 'error').length > 0) { - // if error encountered with configuration, disable neovim - configuration.enableNeovim = false; - } + const validatorResults = await configurationValidator.validate(configuration); // wrap keys this.wrapKeys = {}; @@ -205,7 +138,7 @@ class Configuration implements IConfiguration { VsCodeContext.Set('vim.overrideCopy', this.overrideCopy); VsCodeContext.Set('vim.overrideCtrlC', this.overrideCopy || this.useCtrlKeys); - return configurationErrors; + return validatorResults; } getConfiguration(section: string = ''): vscode.WorkspaceConfiguration { diff --git a/src/configuration/configurationError.ts b/src/configuration/configurationError.ts deleted file mode 100644 index 92ed822ed1b..00000000000 --- a/src/configuration/configurationError.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ConfigurationError { - level: 'error' | 'warning'; - message: string; -} diff --git a/src/configuration/configurationValidator.ts b/src/configuration/configurationValidator.ts index bc348c7c4f6..b94a71cec7f 100644 --- a/src/configuration/configurationValidator.ts +++ b/src/configuration/configurationValidator.ts @@ -1,85 +1,34 @@ -import * as vscode from 'vscode'; -import * as fs from 'fs'; -import { IKeyRemapping } from './iconfiguration'; -import { ConfigurationError } from './configurationError'; -import { promisify } from 'util'; +import { IConfiguration } from './iconfiguration'; +import { IConfigurationValidator, ValidatorResults } from './iconfigurationValidator'; +import { InputMethodSwitcherConfigurationValidator } from './validators/inputMethodSwitcherValidator'; +import { NeovimValidator } from './validators/neovimValidator'; +import { RemappingValidator } from './validators/remappingValidator'; class ConfigurationValidator { - private _commandMap: Map; - - public async isCommandValid(command: string): Promise { - if (command.startsWith(':')) { - return true; - } - - return (await this.getCommandMap()).has(command); + private _validators: IConfigurationValidator[]; + + constructor() { + this._validators = [ + new InputMethodSwitcherConfigurationValidator(), + new NeovimValidator(), + new RemappingValidator(), + ]; } - public async isNeovimValid( - isNeovimEnabled: boolean, - neovimPath: string - ): Promise { - if (isNeovimEnabled) { - try { - const stat = await promisify(fs.stat)(neovimPath); - if (!stat.isFile()) { - return [ - { - level: 'error', - message: `Invalid neovimPath. Please configure full path to nvim binary.`, - }, - ]; - } - } catch (e) { - return [{ level: 'error', message: `Invalid neovimPath. ${e.message}.` }]; - } - } - return []; - } - - public async isRemappingValid(remapping: IKeyRemapping): Promise { - if (!remapping.after && !remapping.commands) { - return [{ level: 'error', message: `${remapping.before} missing 'after' key or 'command'.` }]; - } - - if (!(remapping.before instanceof Array)) { - return [ - { level: 'error', message: `Remapping of '${remapping.before}' should be a string array.` }, - ]; - } - - if (remapping.after && !(remapping.after instanceof Array)) { - return [ - { level: 'error', message: `Remapping of '${remapping.after}' should be a string array.` }, - ]; - } - - if (remapping.commands) { - for (const command of remapping.commands) { - let cmd: string; - - if (typeof command === 'string') { - cmd = command; - } else { - cmd = command.command; - } + public async validate(config: IConfiguration): Promise { + const results = new ValidatorResults(); - if (!(await configurationValidator.isCommandValid(cmd))) { - return [{ level: 'warning', message: `${cmd} does not exist.` }]; - } + for (const validator of this._validators) { + let validatorResults = await validator.validate(config); + if (validatorResults.hasError()) { + // errors found in configuration, disable feature + validator.disable(config); } - } - - return []; - } - async getCommandMap(): Promise> { - if (this._commandMap == null) { - this._commandMap = new Map( - (await vscode.commands.getCommands(true)).map(x => [x, true] as [string, boolean]) - ); + results.concat(validatorResults); } - return this._commandMap; + + return results; } } diff --git a/src/configuration/iconfigurationValidator.ts b/src/configuration/iconfigurationValidator.ts new file mode 100644 index 00000000000..8818e707661 --- /dev/null +++ b/src/configuration/iconfigurationValidator.ts @@ -0,0 +1,35 @@ +import { IConfiguration } from './iconfiguration'; + +interface IValidatorResult { + level: 'error' | 'warning'; + message: string; +} + +export class ValidatorResults { + errors = new Array(); + + public append(validationResult: IValidatorResult) { + this.errors.push(validationResult); + } + + public concat(validationResults: ValidatorResults) { + this.errors = this.errors.concat(validationResults.get()); + } + + public get(): ReadonlyArray { + return this.errors; + } + + public numErrors(): number { + return this.errors.filter(e => e.level === 'error').length; + } + + public hasError(): boolean { + return this.numErrors() > 0; + } +} + +export interface IConfigurationValidator { + validate(config: IConfiguration): Promise; + disable(config: IConfiguration); +} diff --git a/src/configuration/validators/inputMethodSwitcherValidator.ts b/src/configuration/validators/inputMethodSwitcherValidator.ts new file mode 100644 index 00000000000..7305d59fd01 --- /dev/null +++ b/src/configuration/validators/inputMethodSwitcherValidator.ts @@ -0,0 +1,63 @@ +import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; +import { IConfiguration } from '../iconfiguration'; +import { promisify } from 'util'; +import { exists } from 'fs'; +import { Globals } from '../../globals'; + +export class InputMethodSwitcherConfigurationValidator implements IConfigurationValidator { + async validate(config: IConfiguration): Promise { + const result = new ValidatorResults(); + + const inputMethodConfig = config.autoSwitchInputMethod; + + if (!inputMethodConfig.enable || Globals.isTesting) { + return Promise.resolve(result); + } + + if (!inputMethodConfig.switchIMCmd.includes('{im}')) { + result.append({ + level: 'error', + message: + 'vim.autoSwitchInputMethod.switchIMCmd is incorrect, it should contain the placeholder {im}.', + }); + } + + if (inputMethodConfig.obtainIMCmd === undefined || inputMethodConfig.obtainIMCmd === '') { + result.append({ + level: 'error', + message: 'vim.autoSwitchInputMethod.obtainIMCmd is empty.', + }); + } else if (!(await promisify(exists)(this.getRawCmd(inputMethodConfig.obtainIMCmd)))) { + result.append({ + level: 'error', + message: `Unable to find ${ + inputMethodConfig.obtainIMCmd + }. Check your 'vim.autoSwitchInputMethod.obtainIMCmd' in VSCode setting.`, + }); + } + + if (inputMethodConfig.defaultIM === undefined || inputMethodConfig.defaultIM === '') { + result.append({ + level: 'error', + message: 'vim.autoSwitchInputMethod.defaultIM is empty.', + }); + } else if (!(await promisify(exists)(this.getRawCmd(inputMethodConfig.switchIMCmd)))) { + result.append({ + level: 'error', + message: `Unable to find ${ + inputMethodConfig.switchIMCmd + }. Check your 'vim.autoSwitchInputMethod.switchIMCmd' in VSCode setting.`, + }); + } + + return Promise.resolve(result); + } + + disable(config: IConfiguration) { + config.autoSwitchInputMethod.enable = false; + } + + private getRawCmd(cmd: string): string { + return cmd.split(' ')[0]; + } +} diff --git a/src/configuration/validators/neovimValidator.ts b/src/configuration/validators/neovimValidator.ts new file mode 100644 index 00000000000..540958be453 --- /dev/null +++ b/src/configuration/validators/neovimValidator.ts @@ -0,0 +1,33 @@ +import * as fs from 'fs'; +import { IConfiguration } from '../iconfiguration'; +import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; +import { promisify } from 'util'; + +export class NeovimValidator implements IConfigurationValidator { + async validate(config: IConfiguration): Promise { + const result = new ValidatorResults(); + + if (config.enableNeovim) { + try { + const stat = await promisify(fs.stat)(config.neovimPath); + if (!stat.isFile()) { + result.append({ + level: 'error', + message: `Invalid neovimPath. Please configure full path to nvim binary.`, + }); + } + } catch (e) { + result.append({ + level: 'error', + message: `Invalid neovimPath. ${e.message}.`, + }); + } + } + + return Promise.resolve(result); + } + + disable(config: IConfiguration) { + config.enableNeovim = false; + } +} diff --git a/src/configuration/validators/remappingValidator.ts b/src/configuration/validators/remappingValidator.ts new file mode 100644 index 00000000000..37b40711e20 --- /dev/null +++ b/src/configuration/validators/remappingValidator.ts @@ -0,0 +1,132 @@ +import * as vscode from 'vscode'; +import { IConfiguration, IKeyRemapping } from '../iconfiguration'; +import { Notation } from '../notation'; +import { configuration } from '../configuration'; +import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator'; + +export class RemappingValidator implements IConfigurationValidator { + private _commandMap: Map; + + async validate(config: IConfiguration): Promise { + const result = new ValidatorResults(); + const modeKeyBindingsKeys = [ + 'insertModeKeyBindings', + 'insertModeKeyBindingsNonRecursive', + 'normalModeKeyBindings', + 'normalModeKeyBindingsNonRecursive', + 'visualModeKeyBindings', + 'visualModeKeyBindingsNonRecursive', + ]; + for (const modeKeyBindingsKey of modeKeyBindingsKeys) { + let keybindings = config[modeKeyBindingsKey]; + + const modeKeyBindingsMap = new Map(); + for (let i = keybindings.length - 1; i >= 0; i--) { + let remapping = keybindings[i] as IKeyRemapping; + + // validate + let remappingError = await this.isRemappingValid(remapping); + result.concat(remappingError); + if (remappingError.hasError()) { + // errors with remapping, skip + keybindings.splice(i, 1); + continue; + } + + // normalize + if (remapping.before) { + remapping.before.forEach( + (key, idx) => (remapping.before[idx] = Notation.NormalizeKey(key, config.leader)) + ); + } + + if (remapping.after) { + remapping.after.forEach( + (key, idx) => (remapping.after![idx] = Notation.NormalizeKey(key, config.leader)) + ); + } + + // check for duplicates + const beforeKeys = remapping.before.join(''); + if (modeKeyBindingsMap.has(beforeKeys)) { + result.append({ + level: 'error', + message: `${remapping.before}. Duplicate remapped key for ${beforeKeys}.`, + }); + continue; + } + + // add to map + modeKeyBindingsMap.set(beforeKeys, remapping); + } + + configuration[modeKeyBindingsKey + 'Map'] = modeKeyBindingsMap; + } + + return result; + } + + disable(config: IConfiguration) { + // no-op + } + + private async isRemappingValid(remapping: IKeyRemapping): Promise { + const result = new ValidatorResults(); + + if (!remapping.after && !remapping.commands) { + result.append({ + level: 'error', + message: `${remapping.before} missing 'after' key or 'command'.`, + }); + } + + if (!(remapping.before instanceof Array)) { + result.append({ + level: 'error', + message: `Remapping of '${remapping.before}' should be a string array.`, + }); + } + + if (remapping.after && !(remapping.after instanceof Array)) { + result.append({ + level: 'error', + message: `Remapping of '${remapping.after}' should be a string array.`, + }); + } + + if (remapping.commands) { + for (const command of remapping.commands) { + let cmd: string; + + if (typeof command === 'string') { + cmd = command; + } else { + cmd = command.command; + } + + if (!(await this.isCommandValid(cmd))) { + result.append({ level: 'warning', message: `${cmd} does not exist.` }); + } + } + } + + return result; + } + + private async isCommandValid(command: string): Promise { + if (command.startsWith(':')) { + return true; + } + + return (await this.getCommandMap()).has(command); + } + + private async getCommandMap(): Promise> { + if (this._commandMap == null) { + this._commandMap = new Map( + (await vscode.commands.getCommands(true)).map(x => [x, true] as [string, boolean]) + ); + } + return this._commandMap; + } +} diff --git a/src/mode/modeHandler.ts b/src/mode/modeHandler.ts index b4cb2870c10..4b72b0c0118 100644 --- a/src/mode/modeHandler.ts +++ b/src/mode/modeHandler.ts @@ -70,7 +70,7 @@ export class ModeHandler implements vscode.Disposable { new modes.DisabledMode(), ]; - this.vimState = new VimState(textEditor, configuration.enableNeovim); + this.vimState = new VimState(textEditor); this._disposables.push(this.vimState); } diff --git a/src/state/vimState.ts b/src/state/vimState.ts index 628ba163f37..fa85246e69b 100644 --- a/src/state/vimState.ts +++ b/src/state/vimState.ts @@ -207,6 +207,7 @@ export class VimState implements vscode.Disposable { return this._currentMode; } + private _inputMethodSwitcher: InputMethodSwitcher; public async setCurrentMode(value: number): Promise { await this._inputMethodSwitcher.switchInputMethod(this._currentMode, value); this._currentMode = value; @@ -239,9 +240,7 @@ export class VimState implements vscode.Disposable { public nvim: NeovimWrapper; - private _inputMethodSwitcher: InputMethodSwitcher; - - public constructor(editor: vscode.TextEditor, enableNeovim: boolean = false) { + public constructor(editor: vscode.TextEditor) { this.editor = editor; this.identity = new EditorIdentity(editor); this.historyTracker = new HistoryTracker(this); @@ -251,9 +250,7 @@ export class VimState implements vscode.Disposable { } dispose() { - if (this.nvim) { - this.nvim.dispose(); - } + this.nvim.dispose(); } } diff --git a/test/testUtils.ts b/test/testUtils.ts index 47ec84bd823..07bb5e9fa7f 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import { Configuration } from './testConfiguration'; import { Globals } from '../src/globals'; +import { ValidatorResults } from '../src/configuration/iconfigurationValidator'; import { IConfiguration } from '../src/configuration/iconfiguration'; import { TextEditor } from '../src/textEditor'; import { getAndUpdateModeHandler } from '../extension'; @@ -135,7 +136,10 @@ export async function cleanUpWorkspace(): Promise { } export async function reloadConfiguration() { - await require('../src/configuration/configuration').configuration.load(); + let validatorResults = (await require('../src/configuration/configuration').configuration.load()) as ValidatorResults; + for (let validatorResult of validatorResults.get()) { + console.log(validatorResult); + } } /**