Read key remappings from .vimrc (#3908)

Read key remapping commands from $HOME/.vimrc, $HOME/_vimrc, or a
user-specified Vim configuration file. For each, build an IKeyRemapping
object and append it to the appropriate collection, _if_ doing so will
not override a remapping specified in the VS Code settings.

Partially addresses #463. This implementation borrows heavily from
Sheepolution/vimrc-to-json.

* Add `editVimrc` command

* Add .vimrc validator, correct usage of new config names

* Source .vimrc automatically after saving it
This commit is contained in:
Daniel Smith 2019-11-09 23:16:46 -05:00 committed by Jason Fields
parent 66d507bb3d
commit 3158194561
17 changed files with 358 additions and 30 deletions

View File

@ -46,8 +46,6 @@ VSCodeVim is a Vim emulator for [Visual Studio Code](https://code.visualstudio.c
VSCodeVim is automatically enabled following [installation](https://marketplace.visualstudio.com/items?itemName=vscodevim.vim) and reloading of VS Code.
> :warning: Vimscript is _not_ supported; therefore, we are _not_ able to load your `.vimrc` or use `.vim` plugins. You have to replicate these using our [Settings](#settings) and [Emulated plugins](#-emulated-plugins).
### Mac
To enable key-repeating execute the following in your Terminal and restart VS Code:
@ -365,6 +363,12 @@ Configuration settings that have been copied from vim. Vim settings are loaded i
| vim.whichwrap | Controls wrapping at beginning and end of line. Comma-separated set of keys that should wrap to next/previous line. Arrow keys are represented by `[` and `]` in insert mode, `<` and `>` in normal and visual mode. To wrap "everything", set this to `h,l,<,>,[,]`. | String | `` |
| vim.report | Threshold for reporting number of lines changed. | Number | 2 |
## .vimrc support
> :warning: .vimrc support is currently experimental. Only remaps are supported, and you may experience bugs. Please [report them](https://github.com/VSCodeVim/Vim/issues/new?template=bug_report.md)!
Set `vim.vimrc.enable` to `true` and set `vim.vimrc.path` appropriately.
## 🖱️ Multi-Cursor Mode
> :warning: Multi-Cursor mode is experimental. Please report issues in our [feedback thread.](https://github.com/VSCodeVim/Vim/issues/824)

View File

@ -2848,4 +2848,4 @@ The first commit to this project was a little over 3 years ago, and what a journ
- add gulp + tslint [\#6](https://github.com/VSCodeVim/Vim/pull/6) ([jpoon](https://github.com/jpoon))
- command line mode refactoring [\#5](https://github.com/VSCodeVim/Vim/pull/5) ([guillermooo](https://github.com/guillermooo))
- Navigation mode [\#4](https://github.com/VSCodeVim/Vim/pull/4) ([jpoon](https://github.com/jpoon))
- Add ex mode [\#3](https://github.com/VSCodeVim/Vim/pull/3) ([guillermooo](https://github.com/guillermooo))
- Add ex mode [\#3](https://github.com/VSCodeVim/Vim/pull/3) ([guillermooo](https://github.com/guillermooo))

View File

@ -6,6 +6,7 @@
import './src/actions/include-all';
import * as vscode from 'vscode';
import * as path from 'path';
import { CompositionState } from './src/state/compositionState';
import { EditorIdentity } from './src/editorIdentity';
@ -24,6 +25,7 @@ import { configuration } from './src/configuration/configuration';
import { globalState } from './src/state/globalState';
import { taskQueue } from './src/taskQueue';
import { Register } from './src/register/register';
import { vimrc } from './src/configuration/vimrc';
let extensionContext: vscode.ExtensionContext;
let previousActiveEditorId: EditorIdentity | null = null;
@ -87,6 +89,7 @@ async function loadConfiguration() {
}
}
}
export async function activate(context: vscode.ExtensionContext) {
// before we do anything else,
// we need to load the configuration first
@ -207,6 +210,16 @@ export async function activate(context: vscode.ExtensionContext) {
false
);
registerEventListener(context, vscode.workspace.onDidSaveTextDocument, async document => {
if (
configuration.vimrc.enable &&
path.relative(document.fileName, configuration.vimrc.path) === ''
) {
await configuration.load();
vscode.window.showInformationMessage('Sourced new .vimrc');
}
});
// window events
registerEventListener(
context,
@ -391,6 +404,11 @@ export async function activate(context: vscode.ExtensionContext) {
toggleExtension(configuration.disableExtension, compositionState);
});
registerCommand(context, 'vim.editVimrc', async () => {
const document = await vscode.workspace.openTextDocument(configuration.vimrc.path);
await vscode.window.showTextDocument(document);
});
for (const boundKey of configuration.boundKeyCombinations) {
registerCommand(context, boundKey.command, () => handleKeyEvent(`${boundKey.key}`));
}

25
package-lock.json generated
View File

@ -141,20 +141,11 @@
"dev": true
},
"@types/lodash": {
"version": "4.14.138",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.138.tgz",
"integrity": "sha512-A4uJgHz4hakwNBdHNPdxOTkYmXNgmUAKLbXZ7PKGslgeV0Mb8P3BlbYfPovExek1qnod4pDfRbxuzcVs3dlFLg==",
"version": "4.14.144",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.144.tgz",
"integrity": "sha512-ogI4g9W5qIQQUhXAclq6zhqgqNUr7UlFaqDHbch7WLSLeeM/7d3CRaw7GLajxvyFvhJqw4Rpcz5bhoaYtIx6Tg==",
"dev": true
},
"@types/lodash.escaperegexp": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/lodash.escaperegexp/-/lodash.escaperegexp-4.1.6.tgz",
"integrity": "sha512-uENiqxLlqh6RzeE1cC6Z2gHqakToN9vKlTVCFkSVjAfeMeh2fY0916tHwJHeeKs28qB/hGYvKuampGYH5QDVCw==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/mocha": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz",
@ -1888,6 +1879,11 @@
"map-cache": "^0.2.2"
}
},
"fs": {
"version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
"integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ="
},
"fs-mkdirp-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz",
@ -3524,11 +3520,6 @@
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
"dev": true
},
"lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c="
},
"lodash.template": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",

View File

@ -50,6 +50,10 @@
{
"command": "vim.showQuickpickCmdLine",
"title": "Vim: Show Command Line"
},
{
"command": "vim.editVimrc",
"title": "Vim: Edit .vimrc"
}
],
"keybindings": [
@ -750,6 +754,15 @@
"default": "",
"scope": "machine-overridable"
},
"vim.vimrc.enable": {
"type": "boolean",
"description": "Use key mappings from a .vimrc file.",
"default": "true"
},
"vim.vimrc.path": {
"type": "string",
"description": "Path to a Vim configuration file. If unset, it will check for $HOME/.vimrc or $HOME/_vimrc."
},
"vim.substituteGlobalFlag": {
"type": "boolean",
"markdownDescription": "Automatically apply the global flag, `/g`, to substitute commands. When set to true, use `/g` to mean only first match should be replaced.",
@ -897,7 +910,8 @@
},
"dependencies": {
"diff-match-patch": "1.0.4",
"lodash.escaperegexp": "4.1.2",
"fs": "0.0.1-security",
"lodash": "^4.17.15",
"neovim": "4.5.0",
"untildify": "4.0.0",
"winston": "3.2.1",
@ -907,7 +921,7 @@
"devDependencies": {
"@types/diff": "4.0.2",
"@types/diff-match-patch": "1.0.32",
"@types/lodash.escaperegexp": "4.1.6",
"@types/lodash": "^4.14.144",
"@types/mocha": "5.2.7",
"@types/node": "12.12.7",
"@types/sinon": "7.5.0",

View File

@ -6,7 +6,11 @@ export function parseSortCommandArgs(args: string): node.SortCommand {
return new node.SortCommand({ reverse: false, ignoreCase: false, unique: false });
}
let scannedArgs: node.ISortCommandArguments = { reverse: false, ignoreCase: false, unique: false };
let scannedArgs: node.ISortCommandArguments = {
reverse: false,
ignoreCase: false,
unique: false,
};
let scanner = new Scanner(args);
const c = scanner.next();
scannedArgs.reverse = c === '!';

View File

@ -4,7 +4,7 @@ import { VimState } from '../../state/vimState';
import { configuration } from './../../configuration/configuration';
import { VisualBlockMode } from './../../mode/modes';
import { TextEditor } from './../../textEditor';
import escapeRegExp = require('lodash.escaperegexp');
import * as _ from 'lodash';
enum PositionDiffType {
Offset,
@ -874,7 +874,7 @@ export class Position extends vscode.Position {
}
private static makeWordRegex(characterSet: string): RegExp {
let escaped = characterSet && escapeRegExp(characterSet).replace(/-/g, '\\-');
let escaped = characterSet && _.escapeRegExp(characterSet).replace(/-/g, '\\-');
let segments: string[] = [];
segments.push(`([^\\s${escaped}]+)`);
@ -886,7 +886,7 @@ export class Position extends vscode.Position {
}
private static makeCamelCaseWordRegex(characterSet: string): RegExp {
const escaped = characterSet && escapeRegExp(characterSet).replace(/-/g, '\\-');
const escaped = characterSet && _.escapeRegExp(characterSet).replace(/-/g, '\\-');
const segments: string[] = [];
// old versions of VSCode before 1.31 will crash when trying to parse a regex with a lookbehind
@ -1028,7 +1028,7 @@ export class Position extends vscode.Position {
// Symbols in vim.iskeyword or editor.wordSeparators
// are treated as CharKind.Punctuation
const escapedKeywordChars = escapeRegExp(keywordChars).replace(/-/g, '\\-');
const escapedKeywordChars = _.escapeRegExp(keywordChars).replace(/-/g, '\\-');
codePointRangePatterns[Number(CharKind.Punctuation)].push(escapedKeywordChars);
const codePointRanges = codePointRangePatterns.map(patterns => patterns.join(''));

View File

@ -5,6 +5,7 @@ import { ValidatorResults } from './iconfigurationValidator';
import { VsCodeContext } from '../util/vscode-context';
import { configurationValidator } from './configurationValidator';
import { decoration } from './decoration';
import { vimrc } from './vimrc';
import {
IConfiguration,
IKeyRemapping,
@ -86,6 +87,10 @@ class Configuration implements IConfiguration {
}
}
if (this.vimrc.enable) {
vimrc.load(this);
}
this.leader = Notation.NormalizeKey(this.leader, this.leaderDefault);
const validatorResults = await configurationValidator.validate(configuration);
@ -303,6 +308,11 @@ class Configuration implements IConfiguration {
enableNeovim = false;
neovimPath = '';
vimrc = {
enable: false,
path: '',
};
digraphs = {};
gdefault = false;

View File

@ -3,6 +3,7 @@ import { IConfigurationValidator, ValidatorResults } from './iconfigurationValid
import { InputMethodSwitcherConfigurationValidator } from './validators/inputMethodSwitcherValidator';
import { NeovimValidator } from './validators/neovimValidator';
import { RemappingValidator } from './validators/remappingValidator';
import { VimrcValidator } from './validators/vimrcValidator';
class ConfigurationValidator {
private _validators: IConfigurationValidator[];
@ -12,6 +13,7 @@ class ConfigurationValidator {
new InputMethodSwitcherConfigurationValidator(),
new NeovimValidator(),
new RemappingValidator(),
new VimrcValidator(),
];
}

View File

@ -15,6 +15,12 @@ export interface IKeyRemapping {
before: string[];
after?: string[];
commands?: ({ command: string; args: any[] } | string)[];
source?: 'vscode' | 'vimrc';
}
export interface IVimrcKeyRemapping {
keyRemapping: IKeyRemapping;
keyRemappingType: string;
}
export interface IAutoSwitchInputMethod {
@ -287,6 +293,14 @@ export interface IConfiguration {
enableNeovim: boolean;
neovimPath: string;
/**
* .vimrc
*/
vimrc: {
enable: boolean;
path: string;
};
/**
* Automatically apply the `/g` flag to substitute commands.
*/

View File

@ -39,5 +39,5 @@ export class ValidatorResults {
export interface IConfigurationValidator {
validate(config: IConfiguration): Promise<ValidatorResults>;
disable(config: IConfiguration);
disable(config: IConfiguration): void;
}

View File

@ -0,0 +1,23 @@
import * as fs from 'fs';
import { IConfiguration } from '../iconfiguration';
import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator';
import { vimrc } from '../vimrc';
export class VimrcValidator implements IConfigurationValidator {
async validate(config: IConfiguration): Promise<ValidatorResults> {
const result = new ValidatorResults();
if (config.vimrc.enable && !fs.existsSync(vimrc.vimrcPath)) {
result.append({
level: 'error',
message: `.vimrc not found at ${config.vimrc.path}`,
});
}
return result;
}
disable(config: IConfiguration): void {
// no-op
}
}

108
src/configuration/vimrc.ts Normal file
View File

@ -0,0 +1,108 @@
import * as _ from 'lodash';
import * as fs from 'fs';
import * as path from 'path';
import { IConfiguration, IVimrcKeyRemapping } from './iconfiguration';
import { vimrcKeyRemappingBuilder } from './vimrcKeyRemappingBuilder';
class VimrcImpl {
private _vimrcPath: string;
public get vimrcPath(): string {
return this._vimrcPath;
}
public load(config: IConfiguration) {
const _path = config.vimrc.path
? VimrcImpl.expandHome(config.vimrc.path)
: VimrcImpl.findDefaultVimrc();
if (!_path || !fs.existsSync(_path)) {
// TODO: we may want to offer to create the file for them
throw new Error(`Unable to find .vimrc file`);
}
this._vimrcPath = _path;
// Remove all the old remappings from the .vimrc file
VimrcImpl.removeAllRemapsFromConfig(config);
// Add the new remappings
const lines = fs.readFileSync(config.vimrc.path, { encoding: 'utf8' }).split(/\r?\n/);
for (const line of lines) {
const remap = vimrcKeyRemappingBuilder.build(line);
if (remap) {
VimrcImpl.addRemapToConfig(config, remap);
}
}
}
/**
* Adds a remapping from .vimrc to the given configuration
*/
private static addRemapToConfig(config: IConfiguration, remap: IVimrcKeyRemapping): void {
const remaps = (() => {
switch (remap.keyRemappingType) {
case 'nmap':
return config.normalModeKeyBindings;
case 'vmap':
return config.visualModeKeyBindings;
case 'imap':
return config.insertModeKeyBindings;
case 'nnoremap':
return config.normalModeKeyBindingsNonRecursive;
case 'vnoremap':
return config.visualModeKeyBindingsNonRecursive;
case 'inoremap':
return config.insertModeKeyBindingsNonRecursive;
default:
return undefined;
}
})();
// Don't override a mapping present in settings.json; those are more specific to VSCodeVim.
if (remaps && !remaps.some(r => _.isEqual(r.before, remap!.keyRemapping.before))) {
remaps.push(remap.keyRemapping);
}
}
private static removeAllRemapsFromConfig(config: IConfiguration): void {
const remapCollections = [
config.normalModeKeyBindings,
config.visualModeKeyBindings,
config.insertModeKeyBindings,
config.normalModeKeyBindingsNonRecursive,
config.visualModeKeyBindingsNonRecursive,
config.insertModeKeyBindingsNonRecursive,
];
for (const remaps of remapCollections) {
_.remove(remaps, remap => remap.source === 'vimrc');
}
}
private static findDefaultVimrc(): string | undefined {
if (process.env.HOME) {
let vimrcPath = path.join(process.env.HOME, '.vimrc');
if (fs.existsSync(vimrcPath)) {
return vimrcPath;
}
vimrcPath = path.join(process.env.HOME, '_vimrc');
if (fs.existsSync(vimrcPath)) {
return vimrcPath;
}
}
return undefined;
}
private static expandHome(filePath: string): string {
if (!process.env.HOME) {
return filePath;
}
if (!filePath.startsWith('~')) {
return filePath;
}
return path.join(process.env.HOME, filePath.slice(1));
}
}
export const vimrc = new VimrcImpl();

View File

@ -0,0 +1,67 @@
import { IKeyRemapping, IVimrcKeyRemapping } from './iconfiguration';
class VimrcKeyRemappingBuilderImpl {
private static readonly KEY_REMAPPING_REG_EX = /(^.*map)\s([\S]+)\s+([\S]+)$/;
private static readonly KEY_LIST_REG_EX = /(<[^>]+>|.)/g;
private static readonly COMMAND_REG_EX = /(:\w+)/;
/**
* @returns A remapping if the given `line` parses to one, and `undefined` otherwise.
*/
public build(line: string): IVimrcKeyRemapping | undefined {
const matches = VimrcKeyRemappingBuilderImpl.KEY_REMAPPING_REG_EX.exec(line);
if (!matches || matches.length < 4) {
return undefined;
}
const type = matches[1];
const before = matches[2];
const after = matches[3];
let mapping: IKeyRemapping;
if (VimrcKeyRemappingBuilderImpl.isCommand(after)) {
mapping = {
before: VimrcKeyRemappingBuilderImpl.buildKeyList(before),
commands: [after],
source: 'vimrc',
};
} else {
mapping = {
before: VimrcKeyRemappingBuilderImpl.buildKeyList(before),
after: VimrcKeyRemappingBuilderImpl.buildKeyList(after),
source: 'vimrc',
};
}
return {
keyRemapping: mapping,
keyRemappingType: type,
};
}
/**
* @returns `true` if this remaps a key sequence to a `:` command
*/
private static isCommand(commandString: string): boolean {
const matches = VimrcKeyRemappingBuilderImpl.COMMAND_REG_EX.exec(commandString);
if (matches) {
return true;
}
return false;
}
private static buildKeyList(keyString: string): string[] {
let keyList: string[] = [];
let matches: RegExpMatchArray | null = null;
do {
matches = VimrcKeyRemappingBuilderImpl.KEY_LIST_REG_EX.exec(keyString);
if (matches) {
keyList.push(matches[0]);
}
} while (matches);
return keyList;
}
}
export const vimrcKeyRemappingBuilder = new VimrcKeyRemappingBuilderImpl();

View File

@ -161,7 +161,7 @@ suite('cmd_line/search command', () => {
await modeHandler.handleMultipleKeyEvents(['<Esc>', ':', '<C-r>', '<C-w>']);
const statusBar = StatusBar.Get().trim();
assert.equal(statusBar, ':abc|', 'Failed to insert word');
});
});
test('<C-r> <C-w> insert right word of cursor on command line', async () => {
await modeHandler.handleMultipleKeyEvents('i::abc'.split(''));

View File

@ -0,0 +1,69 @@
import * as assert from 'assert';
import { IKeyRemapping, IVimrcKeyRemapping } from '../../src/configuration/iconfiguration';
import { vimrcKeyRemappingBuilder } from '../../src/configuration/vimrcKeyRemappingBuilder';
suite('VimrcKeyRemappingBuilder', () => {
test('Build IKeyRemapping objects from .vimrc lines', () => {
const testCases = [
{
vimrcLine: 'nnoremap <C-h> <<',
keyRemapping: {
before: ['<C-h>'],
after: ['<', '<'],
source: 'vimrc',
},
keyRemappingType: 'nnoremap',
expectNull: false,
},
{
vimrcLine: 'imap jj <Esc>',
keyRemapping: {
before: ['j', 'j'],
after: ['<Esc>'],
source: 'vimrc',
},
keyRemappingType: 'imap',
expectNull: false,
},
{
vimrcLine: 'vnoremap <leader>" c""<Esc>P',
keyRemapping: {
before: ['<leader>', '"'],
after: ['c', '"', '"', '<Esc>', 'P'],
source: 'vimrc',
},
keyRemappingType: 'vnoremap',
expectNull: false,
},
{
// Mapping with a command
vimrcLine: 'nnoremap <C-s> :w',
keyRemapping: {
before: ['<C-s>'],
commands: [':w'],
source: 'vimrc',
},
keyRemappingType: 'nnoremap',
expectNull: false,
},
{
// Ignore non-mapping lines
vimrcLine: 'set scrolloff=8',
expectNull: true,
},
];
for (const testCase of testCases) {
const vimrcKeyRemapping: IVimrcKeyRemapping | undefined = vimrcKeyRemappingBuilder.build(
testCase.vimrcLine
);
if (testCase.expectNull) {
assert.strictEqual(vimrcKeyRemapping, undefined);
} else {
assert.deepStrictEqual(vimrcKeyRemapping!.keyRemapping, testCase.keyRemapping);
assert.strictEqual(vimrcKeyRemapping!.keyRemappingType, testCase.keyRemappingType);
}
}
});
});

View File

@ -81,9 +81,13 @@ export class Configuration implements IConfiguration {
foldfix = false;
disableExtension = false;
enableNeovim = false;
neovimPath = '';
gdefault = false;
substituteGlobalFlag = false; // Deprecated in favor of gdefault
neovimPath = 'nvim';
vimrc = {
enable: false,
path: '',
};
cursorStylePerMode: IModeSpecificStrings<string> = {
normal: 'line',
insert: 'block',