Nodeless entry for Vim extension (#5130)

Add a new entry for running Vim in nodeless environment and load platform-specific modules based on the target.

This PR includes changes to:
- fs. In node, it's node's native fs; in nodeless, it uses vscode.workspace.fs.
- historyFile. In node, it stores the history in file system; in nodeless, it stores in memory.
- logger. In node, it uses winson; in nodeless, it uses browser console.
- lastly, it relies on Webpack to tree shake unwanted code paths (for example, remove nvim related code paths from the bundle in nodeless environment).
This commit is contained in:
Peng Lyu 2020-09-20 10:57:38 -07:00 committed by GitHub
parent dad9c847ce
commit c900a7eaa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 809 additions and 268 deletions

View File

@ -107,7 +107,7 @@ export async function activate(
// Load state
Register.loadFromDisk(extensionContext);
await Promise.all([commandLine.load(), globalState.load()]);
await Promise.all([commandLine.load(extensionContext), globalState.load(extensionContext)]);
if (vscode.window.activeTextEditor) {
const filepathComponents = vscode.window.activeTextEditor.document.fileName.split(/\\|\//);

22
extensionWeb.ts Normal file
View File

@ -0,0 +1,22 @@
/**
* Extension.ts is a lightweight wrapper around ModeHandler. It converts key
* events to their string names and passes them on to ModeHandler via
* handleKeyEvent().
*/
import './src/actions/include-main';
/**
* Load configuration validator
*/
import './src/configuration/validators/inputMethodSwitcherValidator';
import './src/configuration/validators/remappingValidator';
import './src/configuration/validators/vimrcValidator';
import * as vscode from 'vscode';
import { activate as activateFunc } from './extensionBase';
export { getAndUpdateModeHandler } from './extensionBase';
export async function activate(context: vscode.ExtensionContext) {
activateFunc(context, false);
}

View File

@ -9,7 +9,8 @@ var gulp = require('gulp'),
minimist = require('minimist'),
path = require('path'),
webpack_stream = require('webpack-stream'),
webpack_config = require('./webpack.config.js');
webpack_config = require('./webpack.config.js'),
es = require('event-stream');
webpack_dev_config = require('./webpack.dev.js');
const exec = require('child_process').exec;
@ -135,6 +136,29 @@ function updateVersion(done) {
});
}
function updatePath() {
const input = es.through();
const output = input.pipe(
es.mapSync((f) => {
const contents = f.contents.toString('utf8');
const filePath = f.path;
let platformRelativepath = path.relative(
path.dirname(filePath),
path.resolve(process.cwd(), 'out/src/platform/node')
);
f.contents = Buffer.from(
contents.replace(
/\(\"platform\/([^"]*)\"\)/g,
'("' + (platformRelativepath === '' ? './' : platformRelativepath + '/') + '$1")'
),
'utf8'
);
return f;
})
);
return es.duplex(input, output);
}
function copyPackageJson() {
return gulp.src('./package.json').pipe(gulp.dest('out'));
}
@ -156,15 +180,22 @@ gulp.task('tsc', function () {
return tsResult.js
.pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '' }))
.pipe(updatePath())
.pipe(gulp.dest('out'));
});
gulp.task('webpack', function () {
return gulp.src('./extension.ts').pipe(webpack_stream(webpack_config)).pipe(gulp.dest('out'));
return webpack_stream({
config: webpack_config,
entry: ['./extension.ts', './extensionWeb.ts'],
}).pipe(gulp.dest('out'));
});
gulp.task('webpack-dev', function () {
return gulp.src('./extension.ts').pipe(webpack_stream(webpack_dev_config)).pipe(gulp.dest('out'));
return webpack_stream({
config: webpack_dev_config,
entry: ['./extension.ts', './extensionWeb.ts'],
}).pipe(gulp.dest('out'));
});
gulp.task('tslint', function () {

63
package-lock.json generated
View File

@ -2047,6 +2047,12 @@
"integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
"dev": true
},
"duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"dev": true
},
"duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
@ -2382,6 +2388,29 @@
"es5-ext": "~0.10.14"
}
},
"event-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz",
"integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==",
"dev": true,
"requires": {
"duplexer": "^0.1.1",
"from": "^0.1.7",
"map-stream": "0.0.7",
"pause-stream": "^0.0.11",
"split": "^1.0.1",
"stream-combiner": "^0.2.2",
"through": "^2.3.8"
},
"dependencies": {
"map-stream": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz",
"integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=",
"dev": true
}
}
},
"events": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz",
@ -2853,6 +2882,12 @@
"map-cache": "^0.2.2"
}
},
"from": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
"integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
"dev": true
},
"from2": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz",
@ -6455,6 +6490,15 @@
"pinkie-promise": "^2.0.0"
}
},
"pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=",
"dev": true,
"requires": {
"through": "~2.3"
}
},
"pbkdf2": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
@ -7431,6 +7475,15 @@
"integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==",
"dev": true
},
"split": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
"integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
"dev": true,
"requires": {
"through": "2"
}
},
"split-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -7523,6 +7576,16 @@
}
}
},
"stream-combiner": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz",
"integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=",
"dev": true,
"requires": {
"duplexer": "~0.1.1",
"through": "~2.3.4"
}
},
"stream-each": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz",

View File

@ -32,14 +32,17 @@
],
"extensionKind": [
"ui",
"workspace"
"workspace",
"web"
],
"sideEffects": false,
"activationEvents": [
"*",
"onCommand:type"
],
"qna": "https://vscodevim.herokuapp.com/",
"main": "./out/extension",
"browser": "./outWeb/extensionWeb",
"contributes": {
"commands": [
{
@ -1056,6 +1059,7 @@
"@types/sinon": "9.0.5",
"@types/vscode": "1.37.0",
"clean-webpack-plugin": "3.0.0",
"event-stream": "^4.0.1",
"gulp": "4.0.2",
"gulp-bump": "3.2.0",
"gulp-git": "2.10.1",

View File

@ -1,7 +1,26 @@
import * as util from '../../util/util';
import { Logger } from '../../util/logger';
import { Mode } from '../../mode/mode';
import { configuration } from '../../configuration/configuration';
import { exec } from 'child_process';
/**
* This function executes a shell command and returns the standard output as a string.
*/
function executeShell(cmd: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
exec(cmd, (err, stdout, stderr) => {
if (err) {
reject(err);
} else {
resolve(stdout);
}
});
} catch (error) {
reject(error);
}
});
}
/**
* InputMethodSwitcher changes input method when mode changed
@ -11,7 +30,7 @@ export class InputMethodSwitcher {
private execute: (cmd: string) => Promise<string>;
private savedIMKey = '';
constructor(execute: (cmd: string) => Promise<string> = util.executeShell) {
constructor(execute: (cmd: string) => Promise<string> = executeShell) {
this.execute = execute;
}

View File

@ -30,16 +30,13 @@ class CommandLine {
public preCompleteCommand = '';
public get historyEntries() {
return this._history.get();
return this._history?.get() || [];
}
public previousMode = Mode.Normal;
constructor() {
this._history = new CommandLineHistory();
}
public async load(): Promise<void> {
public async load(context: vscode.ExtensionContext): Promise<void> {
this._history = new CommandLineHistory(context);
return this._history.load();
}
@ -66,7 +63,7 @@ class CommandLine {
const cmd = parser.parse(command);
const useNeovim = configuration.enableNeovim && cmd.command && cmd.command.neovimCapable();
if (useNeovim) {
if (useNeovim && vimState.nvim) {
const { statusBarText, error } = await vimState.nvim.run(vimState, command);
StatusBar.setText(vimState, statusBarText, error);
} else {
@ -74,7 +71,11 @@ class CommandLine {
}
} catch (e) {
if (e instanceof VimError) {
if (e.code === ErrorCode.NotAnEditorCommand && configuration.enableNeovim) {
if (
e.code === ErrorCode.NotAnEditorCommand &&
configuration.enableNeovim &&
vimState.nvim
) {
const { statusBarText } = await vimState.nvim.run(vimState, command);
StatusBar.setText(vimState, statusBarText, true);
} else {

View File

@ -1,27 +1,10 @@
import * as fs from 'fs';
import * as util from 'util';
import * as vscode from 'vscode';
import { Logger } from '../../util/logger';
import { getPathDetails, resolveUri } from '../../util/path';
import * as node from '../node';
import { doesFileExist } from 'platform/fs';
import untildify = require('untildify');
async function doesFileExist(fileUri: vscode.Uri) {
const activeTextEditor = vscode.window.activeTextEditor;
if (activeTextEditor) {
try {
await vscode.workspace.fs.stat(fileUri);
return true;
} catch {
return false;
}
} else {
// fallback to local fs
const fsExists = util.promisify(fs.exists);
return fsExists(fileUri.fsPath);
}
}
export enum FilePosition {
NewWindowVerticalSplit,
NewWindowHorizontalSplit,

View File

@ -1,8 +1,7 @@
import { exec } from 'child_process';
import { readFileAsync } from '../../util/fs';
import { TextEditor } from '../../textEditor';
import * as node from '../node';
import { readFileAsync } from 'platform/fs';
import { SUPPORT_READ_COMMAND } from 'platform/constants';
export interface IReadCommandArguments extends node.ICommandArgs {
file?: string;
@ -58,18 +57,24 @@ export class ReadCommand extends node.CommandBase {
}
async getTextToInsertFromCmd(): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
exec(this.arguments.cmd as string, (err, stdout, stderr) => {
if (err) {
reject(err);
} else {
resolve(stdout);
}
});
} catch (e) {
reject(e);
}
});
if (SUPPORT_READ_COMMAND) {
return new Promise<string>((resolve, reject) => {
try {
import('child_process').then((cp) => {
return cp.exec(this.arguments.cmd as string, (err, stdout, stderr) => {
if (err) {
reject(err);
} else {
resolve(stdout);
}
});
});
} catch (e) {
reject(e);
}
});
} else {
return '';
}
}
}

View File

@ -1,4 +1,4 @@
import * as fs from '../../util/fs';
import * as fs from 'platform/fs';
import * as node from '../node';
import * as path from 'path';
import * as vscode from 'vscode';

View File

@ -1,6 +1,6 @@
import { IConfigurationValidator, ValidatorResults } from '../iconfigurationValidator';
import { IConfiguration } from '../iconfiguration';
import { existsAsync } from '../../util/fs';
import { existsAsync } from 'platform/fs';
import { Globals } from '../../globals';
import { configurationValidator } from '../configurationValidator';

View File

@ -1,5 +1,5 @@
import * as _ from 'lodash';
import * as fs from '../util/fs';
import * as fs from 'platform/fs';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';

View File

@ -1,120 +1,56 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { Logger } from '../util/logger';
import { configuration } from '../configuration/configuration';
import { Globals } from '../globals';
import { readFileAsync, mkdirAsync, writeFileAsync, unlinkSync } from '../util/fs';
import { HistoryBase } from 'platform/history';
export class HistoryFile {
private readonly _logger = Logger.get('HistoryFile');
private _historyFileName: string;
private _base: HistoryBase;
private _history: string[] = [];
public get historyFilePath(): string {
return path.join(Globals.extensionStoragePath, this._historyFileName);
get historyFilePath(): string {
return this._base.historyKey;
}
constructor(historyFileName: string) {
this._historyFileName = historyFileName;
constructor(context: vscode.ExtensionContext, historyFileName: string) {
this._base = new HistoryBase(
context,
historyFileName,
Globals.extensionStoragePath,
this._logger
);
}
public async add(value: string | undefined): Promise<void> {
if (!value || value.length === 0) {
return;
}
// remove duplicates
let index: number = this._history.indexOf(value);
if (index !== -1) {
this._history.splice(index, 1);
}
// append to the end
this._history.push(value);
// resize array if necessary
if (this._history.length > configuration.history) {
this._history = this._history.slice(this._history.length - configuration.history);
}
return this.save();
return this._base.add(value, configuration.history);
}
public get(): string[] {
// resize array if necessary
if (this._history.length > configuration.history) {
this._history = this._history.slice(this._history.length - configuration.history);
}
return this._history;
return this._base.get(configuration.history);
}
public clear() {
try {
this._history = [];
unlinkSync(this.historyFilePath);
} catch (err) {
this._logger.warn(`Unable to delete ${this.historyFilePath}. err=${err}.`);
}
this._base.clear();
}
public async load(): Promise<void> {
let data = '';
try {
data = await readFileAsync(this.historyFilePath, 'utf-8');
} catch (err) {
if (err.code === 'ENOENT') {
this._logger.debug(`History does not exist. path=${this.historyFilePath}`);
} else {
this._logger.warn(`Failed to load history. path=${this.historyFilePath} err=${err}.`);
}
return;
}
if (data.length === 0) {
return;
}
try {
let parsedData = JSON.parse(data);
if (!Array.isArray(parsedData)) {
throw Error('Unexpected format in history file. Expected JSON.');
}
this._history = parsedData;
} catch (e) {
this._logger.warn(`Deleting corrupted history file. path=${this.historyFilePath} err=${e}.`);
this.clear();
}
await this._base.load();
}
private async save(): Promise<void> {
try {
// create supplied directory. if directory already exists, do nothing and move on
try {
await mkdirAsync(Globals.extensionStoragePath, { recursive: true });
} catch (createDirectoryErr) {
if (createDirectoryErr.code !== 'EEXIST') {
throw createDirectoryErr;
}
}
// create file
await writeFileAsync(this.historyFilePath, JSON.stringify(this._history), 'utf-8');
} catch (err) {
this._logger.error(`Failed to save history. filepath=${this.historyFilePath}. err=${err}.`);
throw err;
}
await this._base.save();
}
}
export class SearchHistory extends HistoryFile {
constructor() {
super('.search_history');
constructor(context: vscode.ExtensionContext) {
super(context, '.search_history');
}
}
export class CommandLineHistory extends HistoryFile {
constructor() {
super('.cmdline_history');
constructor(context: vscode.ExtensionContext) {
super(context, '.cmdline_history');
}
}

View File

@ -6,7 +6,7 @@ import { VimState } from '../state/vimState';
import { Jump } from './jump';
import { getCursorsAfterSync } from '../util/util';
import { existsAsync } from '../util/fs';
import { existsAsync } from 'platform/fs';
/**
* JumpTracker is a handrolled version of VSCode's TextEditorState

View File

@ -0,0 +1,3 @@
export const SUPPORT_NVIM = false;
export const SUPPORT_IME_SWITCHER = false;
export const SUPPORT_READ_COMMAND = false;

110
src/platform/browser/fs.ts Normal file
View File

@ -0,0 +1,110 @@
import * as vscode from 'vscode';
export const constants = {
UV_FS_SYMLINK_DIR: 1,
UV_FS_SYMLINK_JUNCTION: 2,
O_RDONLY: 0,
O_WRONLY: 1,
O_RDWR: 2,
UV_DIRENT_UNKNOWN: 0,
UV_DIRENT_FILE: 1,
UV_DIRENT_DIR: 2,
UV_DIRENT_LINK: 3,
UV_DIRENT_FIFO: 4,
UV_DIRENT_SOCKET: 5,
UV_DIRENT_CHAR: 6,
UV_DIRENT_BLOCK: 7,
S_IFMT: 61440,
S_IFREG: 32768,
S_IFDIR: 16384,
S_IFCHR: 8192,
S_IFBLK: 24576,
S_IFIFO: 4096,
S_IFLNK: 40960,
S_IFSOCK: 49152,
O_CREAT: 512,
O_EXCL: 2048,
UV_FS_O_FILEMAP: 0,
O_NOCTTY: 131072,
O_TRUNC: 1024,
O_APPEND: 8,
O_DIRECTORY: 1048576,
O_NOFOLLOW: 256,
O_SYNC: 128,
O_DSYNC: 4194304,
O_SYMLINK: 2097152,
O_NONBLOCK: 4,
S_IRWXU: 448,
S_IRUSR: 256,
S_IWUSR: 128,
S_IXUSR: 64,
S_IRWXG: 56,
S_IRGRP: 32,
S_IWGRP: 16,
S_IXGRP: 8,
S_IRWXO: 7,
S_IROTH: 4,
S_IWOTH: 2,
S_IXOTH: 1,
F_OK: 0,
R_OK: 4,
W_OK: 2,
X_OK: 1,
UV_FS_COPYFILE_EXCL: 1,
COPYFILE_EXCL: 1,
UV_FS_COPYFILE_FICLONE: 2,
COPYFILE_FICLONE: 2,
UV_FS_COPYFILE_FICLONE_FORCE: 4,
COPYFILE_FICLONE_FORCE: 4,
};
export async function doesFileExist(fileUri: vscode.Uri) {
try {
await vscode.workspace.fs.stat(fileUri);
return true;
} catch {
return false;
}
}
export async function existsAsync(path: string): Promise<boolean> {
try {
await vscode.workspace.fs.stat(vscode.Uri.parse(path));
return true;
} catch (_e) {
return false;
}
}
export async function unlink(path): Promise<void> {
await vscode.workspace.fs.delete(vscode.Uri.parse(path));
}
export async function readFileAsync(path: string, encoding: string): Promise<string> {
const ret = await vscode.workspace.fs.readFile(vscode.Uri.parse(path));
return ret.toString();
}
export async function mkdirAsync(path: string, options: any): Promise<void> {
return vscode.workspace.fs.createDirectory(vscode.Uri.parse(path));
}
export async function writeFileAsync(
path: string,
content: string,
encoding: string
): Promise<void> {
return vscode.workspace.fs.writeFile(vscode.Uri.parse(path), Buffer.from(content));
}
export async function accessAsync(path: string, mode: number) {
// no op in nodeless
}
export async function chmodAsync(path: string, mode: string | number) {
// no op in nodeless
}
export function unlinkSync(path: string) {
// no op in nodeless
}

View File

@ -0,0 +1,78 @@
import * as vscode from 'vscode';
import { ILogger } from '../common/logger';
export class HistoryBase {
private _historyFileName: string;
private _history: string[] = [];
get historyKey(): string {
return `vim.${this._historyFileName}`;
}
private _context: vscode.ExtensionContext;
private _extensionStoragePath: string;
private _logger: ILogger;
constructor(
context: vscode.ExtensionContext,
historyFileName: string,
extensionStoragePath: string,
logger: ILogger
) {
this._context = context;
this._historyFileName = historyFileName;
this._extensionStoragePath = extensionStoragePath;
this._logger = logger;
}
public async add(value: string | undefined, history: number): Promise<void> {
if (!value || value.length === 0) {
return;
}
// remove duplicates
let index: number = this._history.indexOf(value);
if (index !== -1) {
this._history.splice(index, 1);
}
// append to the end
this._history.push(value);
// resize array if necessary
if (this._history.length > history) {
this._history = this._history.slice(this._history.length - history);
}
return this.save();
}
public get(history: number): string[] {
// resize array if necessary
if (this._history.length > history) {
this._history = this._history.slice(this._history.length - history);
}
return this._history;
}
public async clear() {
this._context.workspaceState.update(this.historyKey, undefined);
this._history = [];
}
public async load(): Promise<void> {
let data = this._context.workspaceState.get<string>(this.historyKey) || '';
if (data.length === 0) {
return;
}
let parsedData = JSON.parse(data);
if (!Array.isArray(parsedData)) {
throw Error('Unexpected format in history. Expected JSON.');
}
this._history = parsedData;
}
async save(): Promise<void> {
this._context.workspaceState.update(this.historyKey, JSON.stringify(this._history));
}
}

View File

@ -0,0 +1,73 @@
import { configuration } from '../../configuration/configuration';
import { ILogger } from 'src/platform/common/logger';
/**
* Displays VSCode message to user
*/
export class VsCodeMessage implements ILogger {
actionMessages = ['Dismiss', 'Suppress Errors'];
private prefix: string;
constructor(prefix: string) {
this.prefix = prefix;
}
error(errorMessage: string): void {
this.log({ level: 'error', message: errorMessage });
}
debug(debugMessage: string): void {
this.log({ level: 'debug', message: debugMessage });
}
warn(warnMessage: string): void {
this.log({ level: 'warn', message: warnMessage });
}
verbose(verboseMessage: string): void {
this.log({ level: 'verbose', message: verboseMessage });
}
private async log(info: { level: string; message: string }) {
if (configuration.debug.silent) {
return;
}
let showMessage: (message: string, ...items: string[]) => void;
switch (info.level) {
case 'error':
showMessage = console.error;
break;
case 'warn':
showMessage = console.warn;
break;
case 'info':
case 'verbose':
case 'debug':
showMessage = console.log;
break;
default:
throw 'Unsupported ' + info.level;
}
let message = info.message;
if (this.prefix) {
message = this.prefix + ': ' + message;
}
showMessage(message, ...this.actionMessages);
}
}
export class LoggerImpl {
static mapping: Map<string, ILogger> = new Map<string, ILogger>();
static get(prefix?: string): ILogger {
prefix = prefix || 'default';
if (LoggerImpl.mapping.has(prefix)) {
return LoggerImpl.mapping.get(prefix)!;
}
const logger = new VsCodeMessage(prefix);
LoggerImpl.mapping.set(prefix, logger);
return logger;
}
}

View File

@ -0,0 +1,6 @@
export interface ILogger {
error(errorMessage: string): void;
debug(debugMessage: string): void;
warn(warnMessage: string): void;
verbose(verboseMessage: string): void;
}

View File

@ -0,0 +1,3 @@
export const SUPPORT_NVIM = true;
export const SUPPORT_IME_SWITCHER = true;
export const SUPPORT_READ_COMMAND = true;

View File

@ -1,5 +1,6 @@
import * as vscode from 'vscode';
import { promisify } from 'util';
import * as util from 'util';
import * as fs from 'fs';
export const constants = {
@ -60,19 +61,33 @@ export const constants = {
COPYFILE_FICLONE_FORCE: 4,
};
export async function doesFileExist(fileUri: vscode.Uri) {
const activeTextEditor = vscode.window.activeTextEditor;
if (activeTextEditor) {
try {
await vscode.workspace.fs.stat(fileUri);
return true;
} catch {
return false;
}
} else {
// fallback to local fs
const fsExists = util.promisify(fs.exists);
return fsExists(fileUri.fsPath);
}
}
export async function existsAsync(path: string): Promise<boolean> {
try {
const uri = vscode.Uri.parse(`file:${path}`);
await vscode.workspace.fs.stat(uri);
await vscode.workspace.fs.stat(vscode.Uri.parse(path));
return true;
} catch (_e) {
return false;
}
}
export async function unlink(path: string): Promise<void> {
const uri = vscode.Uri.parse(`file:${path}`);
await vscode.workspace.fs.delete(uri);
export async function unlink(path): Promise<void> {
fs.unlinkSync(path);
}
export async function readFileAsync(path: string, encoding: string): Promise<string> {

View File

@ -0,0 +1,120 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { readFileAsync, mkdirAsync, writeFileAsync, unlinkSync } from 'platform/fs';
import { ILogger } from '../common/logger';
import { Globals } from '../../globals';
export class HistoryBase {
private _historyFileName: string;
private _history: string[] = [];
get historyKey(): string {
return path.join(this._extensionStoragePath, this._historyFileName);
}
private _context: vscode.ExtensionContext;
private _extensionStoragePath: string;
private _logger: ILogger;
constructor(
context: vscode.ExtensionContext,
historyFileName: string,
extensionStoragePath: string,
logger: ILogger
) {
this._historyFileName = historyFileName;
this._context = context;
this._extensionStoragePath = extensionStoragePath;
this._logger = logger;
}
public async add(value: string | undefined, history: number): Promise<void> {
if (!value || value.length === 0) {
return;
}
// remove duplicates
let index: number = this._history.indexOf(value);
if (index !== -1) {
this._history.splice(index, 1);
}
// append to the end
this._history.push(value);
// resize array if necessary
if (this._history.length > history) {
this._history = this._history.slice(this._history.length - history);
}
return this.save();
}
public get(history: number): string[] {
// resize array if necessary
if (this._history.length > history) {
this._history = this._history.slice(this._history.length - history);
}
return this._history;
}
public clear() {
try {
this._history = [];
unlinkSync(this.historyKey);
} catch (err) {
this._logger.warn(`Unable to delete ${this.historyKey}. err=${err}.`);
}
}
public async load(): Promise<void> {
// await this._base.load();
let data = '';
try {
data = await readFileAsync(this.historyKey, 'utf-8');
} catch (err) {
if (err.code === 'ENOENT') {
this._logger.debug(`History does not exist. path=${this.historyKey}`);
} else {
this._logger.warn(`Failed to load history. path=${this.historyKey} err=${err}.`);
}
return;
}
if (data.length === 0) {
return;
}
try {
let parsedData = JSON.parse(data);
if (!Array.isArray(parsedData)) {
throw Error('Unexpected format in history file. Expected JSON.');
}
this._history = parsedData;
} catch (e) {
this._logger.warn(`Deleting corrupted history file. path=${this.historyKey} err=${e}.`);
this.clear();
}
}
async save(): Promise<void> {
try {
// create supplied directory. if directory already exists, do nothing and move on
try {
await mkdirAsync(Globals.extensionStoragePath, { recursive: true });
} catch (createDirectoryErr) {
if (createDirectoryErr.code !== 'EEXIST') {
throw createDirectoryErr;
}
}
// create file
await writeFileAsync(this.historyKey, JSON.stringify(this._history), 'utf-8');
} catch (err) {
this._logger.error(`Failed to save history. filepath=${this.historyKey}. err=${err}.`);
throw err;
}
}
}

View File

@ -0,0 +1,83 @@
import * as TransportStream from 'winston-transport';
import * as vscode from 'vscode';
import * as winston from 'winston';
import { ConsoleForElectron } from 'winston-console-for-electron';
import { configuration } from '../../configuration/configuration';
interface VsCodeMessageOptions extends TransportStream.TransportStreamOptions {
prefix?: string;
}
/**
* Implementation of Winston transport
* Displays VSCode message to user
*/
class VsCodeMessage extends TransportStream {
prefix?: string;
actionMessages = ['Dismiss', 'Suppress Errors'];
constructor(options: VsCodeMessageOptions) {
super(options);
this.prefix = options.prefix;
}
public async log(info: { level: string; message: string }, callback: Function) {
if (configuration.debug.silent) {
return;
}
let showMessage: (message: string, ...items: string[]) => Thenable<string | undefined>;
switch (info.level) {
case 'error':
showMessage = vscode.window.showErrorMessage;
break;
case 'warn':
showMessage = vscode.window.showWarningMessage;
break;
case 'info':
case 'verbose':
case 'debug':
showMessage = vscode.window.showInformationMessage;
break;
default:
throw 'Unsupported ' + info.level;
}
let message = info.message;
if (this.prefix) {
message = this.prefix + ': ' + message;
}
let selectedAction = await showMessage(message, ...this.actionMessages);
if (selectedAction === 'Suppress Errors') {
vscode.window.showInformationMessage(
'Ignorance is bliss; temporarily suppressing log messages. For more permanence, please configure `vim.debug.silent`.'
);
configuration.debug.silent = true;
}
if (callback) {
callback();
}
}
}
export class LoggerImpl {
static get(prefix?: string): winston.Logger {
return winston.createLogger({
format: winston.format.simple(),
transports: [
// TODO: update these when configuration changes
new ConsoleForElectron({
level: configuration.debug.loggingLevelForConsole,
silent: configuration.debug.silent,
prefix: prefix,
}),
new VsCodeMessage({
level: configuration.debug.loggingLevelForAlert,
prefix: prefix,
}),
],
});
}
}

View File

@ -53,8 +53,8 @@ class GlobalState {
*/
public hl = true;
public async load() {
this._searchHistory = new SearchHistory();
public async load(context: vscode.ExtensionContext) {
this._searchHistory = new SearchHistory(context);
this._searchHistory
.get()
.forEach((val) =>

View File

@ -5,7 +5,6 @@ import { configuration } from '../configuration/configuration';
import { EasyMotion } from './../actions/plugins/easymotion/easymotion';
import { EditorIdentity } from './../editorIdentity';
import { HistoryTracker } from './../history/historyTracker';
import { InputMethodSwitcher } from '../actions/plugins/imswitcher';
import { Logger } from '../util/logger';
import { Mode } from '../mode/mode';
import { Position } from './../common/motion/position';
@ -15,6 +14,11 @@ import { RegisterMode } from './../register/register';
import { ReplaceState } from './../state/replaceState';
import { IKeyRemapping } from '../configuration/iconfiguration';
import { SurroundState } from '../actions/plugins/surround';
import { SUPPORT_NVIM, SUPPORT_IME_SWITCHER } from 'platform/constants';
interface IInputMethodSwitcher {
switchInputMethod(prevMode: Mode, newMode: Mode);
}
interface INVim {
run(vimState: VimState, command: string): Promise<{ statusBarText: string; error: boolean }>;
@ -331,6 +335,7 @@ export class VimState implements vscode.Disposable {
return this._currentMode;
}
private _inputMethodSwitcher?: IInputMethodSwitcher;
/**
* The mode Vim is currently including pseudo-modes like OperatorPendingMode
* This is to be used only by the Remappers when getting the remappings so don't
@ -342,9 +347,8 @@ export class VimState implements vscode.Disposable {
: this._currentMode;
}
private _inputMethodSwitcher: InputMethodSwitcher;
public async setCurrentMode(mode: Mode): Promise<void> {
await this._inputMethodSwitcher.switchInputMethod(this._currentMode, mode);
await this._inputMethodSwitcher?.switchInputMethod(this._currentMode, mode);
if (this.returnToInsertAfterCommand && mode === Mode.Insert) {
this.returnToInsertAfterCommand = false;
}
@ -393,23 +397,29 @@ export class VimState implements vscode.Disposable {
/** The macro currently being recorded, if one exists. */
public macro: RecordedState | undefined;
public nvim: INVim;
public nvim?: INVim;
public constructor(editor: vscode.TextEditor) {
this.editor = editor;
this.identity = EditorIdentity.fromEditor(editor);
this.historyTracker = new HistoryTracker(this);
this.easyMotion = new EasyMotion();
this._inputMethodSwitcher = new InputMethodSwitcher();
}
async load() {
const m = await import('../neovim/neovim');
this.nvim = new m.NeovimWrapper();
if (SUPPORT_NVIM) {
const m = await import('../neovim/neovim');
this.nvim = new m.NeovimWrapper();
}
if (SUPPORT_IME_SWITCHER) {
const ime = await import('../actions/plugins/imswitcher');
this._inputMethodSwitcher = new ime.InputMethodSwitcher();
}
}
dispose() {
this.nvim.dispose();
this.nvim?.dispose();
}
}

View File

@ -11,7 +11,7 @@ export class Clipboard {
try {
await vscode.env.clipboard.writeText(text);
} catch (e) {
this.logger.error(e, `Error copying to clipboard. err=${e}`);
this.logger.error(`Error copying to clipboard. err=${e}`);
}
}

View File

@ -1,8 +1,8 @@
import { exec } from '../util/child_process';
import { readFileAsync, writeFileAsync, unlink } from '../util/fs';
import { readFileAsync, writeFileAsync, unlink } from 'platform/fs';
import { tmpdir } from '../util/os';
import { join } from '../util/path';
import { VimError, ErrorCode } from '../error';
import { promisify } from 'util';
class ExternalCommand {
private previousExternalCommand: string | undefined;
@ -96,7 +96,9 @@ class ExternalCommand {
this.previousExternalCommand = command;
command = this.redirectCommand(command, inputFile, outputFile);
try {
await exec(command);
await import('child_process').then((cp) => {
return promisify(cp.exec)(command);
});
} catch (e) {
// exec throws an error if exit code != 0
// keep going and read the output anyway (just like vim)

View File

@ -1,83 +1,8 @@
import * as TransportStream from 'winston-transport';
import * as vscode from 'vscode';
import * as winston from 'winston';
import { ConsoleForElectron } from 'winston-console-for-electron';
import { configuration } from '../configuration/configuration';
interface VsCodeMessageOptions extends TransportStream.TransportStreamOptions {
prefix?: string;
}
/**
* Implementation of Winston transport
* Displays VSCode message to user
*/
class VsCodeMessage extends TransportStream {
prefix?: string;
actionMessages = ['Dismiss', 'Suppress Errors'];
constructor(options: VsCodeMessageOptions) {
super(options);
this.prefix = options.prefix;
}
public async log(info: { level: string; message: string }, callback: Function) {
if (configuration.debug.silent) {
return;
}
let showMessage: (message: string, ...items: string[]) => Thenable<string | undefined>;
switch (info.level) {
case 'error':
showMessage = vscode.window.showErrorMessage;
break;
case 'warn':
showMessage = vscode.window.showWarningMessage;
break;
case 'info':
case 'verbose':
case 'debug':
showMessage = vscode.window.showInformationMessage;
break;
default:
throw 'Unsupported ' + info.level;
}
let message = info.message;
if (this.prefix) {
message = this.prefix + ': ' + message;
}
let selectedAction = await showMessage(message, ...this.actionMessages);
if (selectedAction === 'Suppress Errors') {
vscode.window.showInformationMessage(
'Ignorance is bliss; temporarily suppressing log messages. For more permanence, please configure `vim.debug.silent`.'
);
configuration.debug.silent = true;
}
if (callback) {
callback();
}
}
}
import { ILogger } from 'src/platform/common/logger';
import { LoggerImpl } from 'platform/loggerImpl';
export class Logger {
static get(prefix?: string): winston.Logger {
return winston.createLogger({
format: winston.format.simple(),
transports: [
// TODO: update these when configuration changes
new ConsoleForElectron({
level: configuration.debug.loggingLevelForConsole,
silent: configuration.debug.silent,
prefix: prefix,
}),
new VsCodeMessage({
level: configuration.debug.loggingLevelForAlert,
prefix: prefix,
}),
],
});
static get(prefix?: string): ILogger {
return LoggerImpl.get(prefix);
}
}

View File

@ -1,6 +1,5 @@
import * as vscode from 'vscode';
import { Range } from '../common/motion/range';
import { exec } from 'child_process';
import { VimState } from '../state/vimState';
/**
@ -13,25 +12,6 @@ export function getCursorsAfterSync(): Range[] {
return vscode.window.activeTextEditor!.selections.map((x) => Range.FromVSCodeSelection(x));
}
/**
* This function executes a shell command and returns the standard output as a string.
*/
export function executeShell(cmd: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
try {
exec(cmd, (err, stdout, stderr) => {
if (err) {
reject(err);
} else {
resolve(stdout);
}
});
} catch (error) {
reject(error);
}
});
}
export function clamp(num: number, min: number, max: number) {
return Math.min(Math.max(num, min), max);
}

View File

@ -27,6 +27,7 @@ suite('base action', () => {
suiteSetup(async () => {
await setupWorkspace();
vimState = new VimState(vscode.window.activeTextEditor!);
await vimState.load();
});
suiteTeardown(cleanUpWorkspace);

View File

@ -2,7 +2,7 @@ import * as assert from 'assert';
import * as fs from 'fs';
import * as os from 'os';
import { HistoryFile } from '../../src/history/historyFile';
import { setupWorkspace, cleanUpWorkspace, rndName } from '../testUtils';
import { setupWorkspace, cleanUpWorkspace, rndName, TestExtensionContext } from '../testUtils';
import { configuration } from '../../src/configuration/configuration';
import { Globals } from '../../src/globals';
@ -26,7 +26,7 @@ suite('HistoryFile', () => {
}
Globals.extensionStoragePath = os.tmpdir();
history = new HistoryFile(rndName());
history = new HistoryFile(new TestExtensionContext(), rndName());
await history.load();
});

View File

@ -245,6 +245,7 @@ suite('register', () => {
test('Can put and get to register', async () => {
const expected = 'text-to-put-on-register';
const vimState = new VimState(vscode.window.activeTextEditor!);
await vimState.load();
vimState.recordedState.registerName = '0';
try {

View File

@ -12,9 +12,10 @@ suite('VimState', () => {
teardown(cleanUpWorkspace);
test('de-dupes cursors', () => {
test('de-dupes cursors', async () => {
// setup
const vimState = new VimState(vscode.window.activeTextEditor!);
await vimState.load();
const cursorStart = new Position(0, 0);
const cursorStop = new Position(0, 1);
const initialCursors = [new Range(cursorStart, cursorStop), new Range(cursorStart, cursorStop)];
@ -26,9 +27,10 @@ suite('VimState', () => {
assert.strictEqual(vimState.cursors.length, 1);
});
test('cursorStart/cursorStop should be first cursor in cursors', () => {
test('cursorStart/cursorStop should be first cursor in cursors', async () => {
// setup
const vimState = new VimState(vscode.window.activeTextEditor!);
await vimState.load();
const cursorStart = new Position(0, 0);
const cursorStop = new Position(0, 1);
const initialCursors = [

View File

@ -124,7 +124,7 @@ export async function setupWorkspace(
config: IConfiguration = new Configuration(),
fileExtension: string = ''
): Promise<void> {
await commandLine.load();
await commandLine.load(new TestExtensionContext());
const filePath = await createRandomFile('', fileExtension);
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(filePath));

View File

@ -12,6 +12,10 @@
"strictNullChecks": true,
"experimentalDecorators": true,
"alwaysStrict": true,
"baseUrl": ".",
"paths": {
"platform/*": ["src/platform/node/*"]
},
"resolveJsonModule": true
},
"exclude": ["node_modules", "!node_modules/@types"]

View File

@ -3,6 +3,7 @@
'use strict';
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
/**@type {import('webpack').Configuration}*/
@ -26,6 +27,9 @@ const config = {
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js'],
alias: {
platform: path.resolve(__dirname, 'src', 'platform', 'node'),
},
},
module: {
rules: [
@ -36,6 +40,61 @@ const config = {
},
],
},
plugins: [new CleanWebpackPlugin()],
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [], // disable initial clean
}),
],
};
module.exports = config;
/**@type {import('webpack').Configuration}*/
const nodelessConfig = {
target: 'webworker', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'development',
entry: './extensionWeb.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
output: {
// the bundle is stored in the 'out' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'out'),
filename: 'extensionWeb.js',
libraryTarget: 'umd',
},
devtool: 'inline-source-map',
externals: {
vscode: 'commonjs vscode', // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js'],
alias: {
platform: path.resolve(__dirname, 'src', 'platform', 'browser'),
},
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
},
],
},
],
},
plugins: [
new webpack.IgnorePlugin({
resourceRegExp: /\/neovim$/,
}),
new webpack.IgnorePlugin({
resourceRegExp: /\/imswitcher$/,
}),
new webpack.IgnorePlugin({
resourceRegExp: /child_process$/,
}),
],
};
module.exports = [config, nodelessConfig];

View File

@ -1,7 +1,9 @@
const merge = require('webpack-merge');
const prod_config = require('./webpack.config.js');
const prod_configs = require('./webpack.config.js');
module.exports = merge.merge(prod_config, {
mode: 'development',
devtool: 'inline-source-map',
});
module.exports = prod_configs.map((config) =>
merge.merge(prod_configs[0], {
mode: 'development',
devtool: 'inline-source-map',
})
);