diff --git a/README.md b/README.md index 216d93950..a801996fa 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/build/CHANGELOG.base.md b/build/CHANGELOG.base.md index a7f519b78..f8556afcb 100644 --- a/build/CHANGELOG.base.md +++ b/build/CHANGELOG.base.md @@ -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)) \ No newline at end of file +- Add ex mode [\#3](https://github.com/VSCodeVim/Vim/pull/3) ([guillermooo](https://github.com/guillermooo)) diff --git a/extension.ts b/extension.ts index 12e818372..fe1619b60 100644 --- a/extension.ts +++ b/extension.ts @@ -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}`)); } diff --git a/package-lock.json b/package-lock.json index df6df4edd..b23d89464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 62d1cea6c..b7fa598d0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cmd_line/subparsers/sort.ts b/src/cmd_line/subparsers/sort.ts index 19a561e11..85afca2c8 100644 --- a/src/cmd_line/subparsers/sort.ts +++ b/src/cmd_line/subparsers/sort.ts @@ -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 === '!'; diff --git a/src/common/motion/position.ts b/src/common/motion/position.ts index 6127590aa..ed5fc92e6 100644 --- a/src/common/motion/position.ts +++ b/src/common/motion/position.ts @@ -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('')); diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 3dc417bdc..7d0dde426 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -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; diff --git a/src/configuration/configurationValidator.ts b/src/configuration/configurationValidator.ts index 6d29ddc12..0d7e0684c 100644 --- a/src/configuration/configurationValidator.ts +++ b/src/configuration/configurationValidator.ts @@ -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(), ]; } diff --git a/src/configuration/iconfiguration.ts b/src/configuration/iconfiguration.ts index 1a1c2e262..81db1b10d 100644 --- a/src/configuration/iconfiguration.ts +++ b/src/configuration/iconfiguration.ts @@ -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. */ diff --git a/src/configuration/iconfigurationValidator.ts b/src/configuration/iconfigurationValidator.ts index 73a004dba..61b56ebd3 100644 --- a/src/configuration/iconfigurationValidator.ts +++ b/src/configuration/iconfigurationValidator.ts @@ -39,5 +39,5 @@ export class ValidatorResults { export interface IConfigurationValidator { validate(config: IConfiguration): Promise; - disable(config: IConfiguration); + disable(config: IConfiguration): void; } diff --git a/src/configuration/validators/vimrcValidator.ts b/src/configuration/validators/vimrcValidator.ts new file mode 100644 index 000000000..09498daec --- /dev/null +++ b/src/configuration/validators/vimrcValidator.ts @@ -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 { + 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 + } +} diff --git a/src/configuration/vimrc.ts b/src/configuration/vimrc.ts new file mode 100644 index 000000000..2f3104d3d --- /dev/null +++ b/src/configuration/vimrc.ts @@ -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(); diff --git a/src/configuration/vimrcKeyRemappingBuilder.ts b/src/configuration/vimrcKeyRemappingBuilder.ts new file mode 100644 index 000000000..25459b72c --- /dev/null +++ b/src/configuration/vimrcKeyRemappingBuilder.ts @@ -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(); diff --git a/test/cmd_line/command.test.ts b/test/cmd_line/command.test.ts index a60463e31..2afa91cb5 100644 --- a/test/cmd_line/command.test.ts +++ b/test/cmd_line/command.test.ts @@ -161,7 +161,7 @@ suite('cmd_line/search command', () => { await modeHandler.handleMultipleKeyEvents(['', ':', '', '']); const statusBar = StatusBar.Get().trim(); assert.equal(statusBar, ':abc|', 'Failed to insert word'); - }); + }); test(' insert right word of cursor on command line', async () => { await modeHandler.handleMultipleKeyEvents('i::abc'.split('')); diff --git a/test/configuration/vimrcKeyRemappingBuilder.test.ts b/test/configuration/vimrcKeyRemappingBuilder.test.ts new file mode 100644 index 000000000..37e2e4c03 --- /dev/null +++ b/test/configuration/vimrcKeyRemappingBuilder.test.ts @@ -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 <<', + keyRemapping: { + before: [''], + after: ['<', '<'], + source: 'vimrc', + }, + keyRemappingType: 'nnoremap', + expectNull: false, + }, + { + vimrcLine: 'imap jj ', + keyRemapping: { + before: ['j', 'j'], + after: [''], + source: 'vimrc', + }, + keyRemappingType: 'imap', + expectNull: false, + }, + { + vimrcLine: 'vnoremap " c""P', + keyRemapping: { + before: ['', '"'], + after: ['c', '"', '"', '', 'P'], + source: 'vimrc', + }, + keyRemappingType: 'vnoremap', + expectNull: false, + }, + { + // Mapping with a command + vimrcLine: 'nnoremap :w', + keyRemapping: { + before: [''], + 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); + } + } + }); +}); diff --git a/test/testConfiguration.ts b/test/testConfiguration.ts index ddbbf5129..1208fb593 100644 --- a/test/testConfiguration.ts +++ b/test/testConfiguration.ts @@ -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 = { normal: 'line', insert: 'block',