SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export interface IBadge {
getDescription(): string;
}
export class BaseBadge implements IBadge {
public descriptorFn: (args: any) => string;
constructor(descriptorFn: (args: any) => string) {
this.descriptorFn = descriptorFn;
}
public getDescription(): string {
return this.descriptorFn(null);
}
}
export class NumberBadge extends BaseBadge {
public number: number;
constructor(number: number, descriptorFn: (args: any) => string) {
super(descriptorFn);
this.number = number;
}
public getDescription(): string {
return this.descriptorFn(this.number);
}
}
export class TextBadge extends BaseBadge {
public text: string;
constructor(text: string, descriptorFn: (args: any) => string) {
super(descriptorFn);
this.text = text;
}
}
export class IconBadge extends BaseBadge {
constructor(descriptorFn: (args: any) => string) {
super(descriptorFn);
}
}
export class ProgressBadge extends BaseBadge {
}
export const IActivityBarService = createDecorator<IActivityBarService>('activityBarService');
export interface IActivityBarService {
_serviceBrand: any;
/**
* Show activity in the activitybar for the given global activity.
*/
showGlobalActivity(globalActivityId: string, badge: IBadge): IDisposable;
/**
* Show activity in the activitybar for the given viewlet.
*/
showActivity(viewletId: string, badge: IBadge, clazz?: string): IDisposable;
/**
* Unpins a viewlet from the activitybar.
*/
unpin(viewletId: string): void;
/**
* Pin a viewlet inside the activity bar.
*/
pin(viewletId: string): void;
/**
* Find out if a viewlet is pinned in the activity bar.
*/
isPinned(viewletId: string): boolean;
/**
* Reorder viewlet ordering by moving a viewlet to the location of another viewlet.
*/
move(viewletId: string, toViewletId: string): void;
}

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import Uri from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TPromise } from 'vs/base/common/winjs.base';
import { IResolveContentOptions, IUpdateContentOptions } from 'vs/platform/files/common/files';
import { IRawTextSource } from 'vs/editor/common/model/textSource';
export const IBackupFileService = createDecorator<IBackupFileService>('backupFileService');
export const BACKUP_FILE_RESOLVE_OPTIONS: IResolveContentOptions = { acceptTextOnly: true, encoding: 'utf-8' };
export const BACKUP_FILE_UPDATE_OPTIONS: IUpdateContentOptions = { encoding: 'utf-8' };
/**
* A service that handles any I/O and state associated with the backup system.
*/
export interface IBackupFileService {
_serviceBrand: any;
/**
* If backups are enabled.
*/
backupEnabled: boolean;
/**
* Finds out if there are any backups stored.
*/
hasBackups(): TPromise<boolean>;
/**
* Loads the backup resource for a particular resource within the current workspace.
*
* @param resource The resource that is backed up.
* @return The backup resource if any.
*/
loadBackupResource(resource: Uri): TPromise<Uri>;
/**
* Backs up a resource.
*
* @param resource The resource to back up.
* @param content The content of the resource.
* @param versionId The version id of the resource to backup.
*/
backupResource(resource: Uri, content: string, versionId?: number): TPromise<void>;
/**
* Gets a list of file backups for the current workspace.
*
* @return The list of backups.
*/
getWorkspaceFileBackups(): TPromise<Uri[]>;
/**
* Parses backup raw text content into the content, removing the metadata that is also stored
* in the file.
*
* @param rawText The IRawTextProvider from a backup resource.
* @return The backup file's backed up content.
*/
parseBackupContent(textSource: IRawTextSource): string;
/**
* Discards the backup associated with a resource if it exists..
*
* @param resource The resource whose backup is being discarded discard to back up.
*/
discardResourceBackup(resource: Uri): TPromise<void>;
/**
* Discards all backups associated with the current workspace and prevents further backups from
* being made.
*/
discardAllWorkspaceBackups(): TPromise<void>;
}

View File

@@ -0,0 +1,268 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as path from 'path';
import * as crypto from 'crypto';
import * as platform from 'vs/base/common/platform';
import pfs = require('vs/base/node/pfs');
import Uri from 'vs/base/common/uri';
import { Queue } from 'vs/base/common/async';
import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS } from 'vs/workbench/services/backup/common/backup';
import { IFileService } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
import { readToMatchingString } from 'vs/base/node/stream';
import { TextSource, IRawTextSource } from 'vs/editor/common/model/textSource';
import { DefaultEndOfLine } from 'vs/editor/common/editorCommon';
export interface IBackupFilesModel {
resolve(backupRoot: string): TPromise<IBackupFilesModel>;
add(resource: Uri, versionId?: number): void;
has(resource: Uri, versionId?: number): boolean;
get(): Uri[];
remove(resource: Uri): void;
count(): number;
clear(): void;
}
export class BackupFilesModel implements IBackupFilesModel {
private cache: { [resource: string]: number /* version ID */ } = Object.create(null);
public resolve(backupRoot: string): TPromise<IBackupFilesModel> {
return pfs.readDirsInDir(backupRoot).then(backupSchemas => {
// For all supported schemas
return TPromise.join(backupSchemas.map(backupSchema => {
// Read backup directory for backups
const backupSchemaPath = path.join(backupRoot, backupSchema);
return pfs.readdir(backupSchemaPath).then(backupHashes => {
// Remember known backups in our caches
backupHashes.forEach(backupHash => {
const backupResource = Uri.file(path.join(backupSchemaPath, backupHash));
this.add(backupResource);
});
});
}));
}).then(() => this, error => this);
}
public add(resource: Uri, versionId = 0): void {
this.cache[resource.toString()] = versionId;
}
public count(): number {
return Object.keys(this.cache).length;
}
public has(resource: Uri, versionId?: number): boolean {
const cachedVersionId = this.cache[resource.toString()];
if (typeof cachedVersionId !== 'number') {
return false; // unknown resource
}
if (typeof versionId === 'number') {
return versionId === cachedVersionId; // if we are asked with a specific version ID, make sure to test for it
}
return true;
}
public get(): Uri[] {
return Object.keys(this.cache).map(k => Uri.parse(k));
}
public remove(resource: Uri): void {
delete this.cache[resource.toString()];
}
public clear(): void {
this.cache = Object.create(null);
}
}
export class BackupFileService implements IBackupFileService {
public _serviceBrand: any;
private static readonly META_MARKER = '\n';
private isShuttingDown: boolean;
private ready: TPromise<IBackupFilesModel>;
/**
* Ensure IO operations on individual files are performed in order, this could otherwise lead
* to unexpected behavior when backups are persisted and discarded in the wrong order.
*/
private ioOperationQueues: { [path: string]: Queue<void> };
constructor(
private backupWorkspacePath: string,
@IFileService private fileService: IFileService
) {
this.isShuttingDown = false;
this.ioOperationQueues = {};
this.ready = this.init();
}
public get backupEnabled(): boolean {
return !!this.backupWorkspacePath; // Hot exit requires a backup path
}
private init(): TPromise<IBackupFilesModel> {
const model = new BackupFilesModel();
if (!this.backupEnabled) {
return TPromise.as(model);
}
return model.resolve(this.backupWorkspacePath);
}
public hasBackups(): TPromise<boolean> {
return this.ready.then(model => {
return model.count() > 0;
});
}
public loadBackupResource(resource: Uri): TPromise<Uri> {
return this.ready.then(model => {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return void 0;
}
// Return directly if we have a known backup with that resource
if (model.has(backupResource)) {
return backupResource;
}
// Otherwise: on Windows and Mac pre v1.11 we used to store backups in lowercase format
// Therefor we also want to check if we have backups of this old format hanging around
// TODO@Ben migration
if (platform.isWindows || platform.isMacintosh) {
const legacyBackupResource = this.getBackupResource(resource, true /* legacyMacWindowsFormat */);
if (model.has(legacyBackupResource)) {
return legacyBackupResource;
}
}
return void 0;
});
}
public backupResource(resource: Uri, content: string, versionId?: number): TPromise<void> {
if (this.isShuttingDown) {
return TPromise.as(void 0);
}
return this.ready.then(model => {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return void 0;
}
if (model.has(backupResource, versionId)) {
return void 0; // return early if backup version id matches requested one
}
// Add metadata to top of file
content = `${resource.toString()}${BackupFileService.META_MARKER}${content}`;
return this.getResourceIOQueue(backupResource).queue(() => {
return this.fileService.updateContent(backupResource, content, BACKUP_FILE_UPDATE_OPTIONS).then(() => model.add(backupResource, versionId));
});
});
}
public discardResourceBackup(resource: Uri): TPromise<void> {
return this.ready.then(model => {
const backupResource = this.getBackupResource(resource);
if (!backupResource) {
return void 0;
}
return this.getResourceIOQueue(backupResource).queue(() => {
return pfs.del(backupResource.fsPath).then(() => model.remove(backupResource));
}).then(() => {
// On Windows and Mac pre v1.11 we used to store backups in lowercase format
// Therefor we also want to check if we have backups of this old format laying around
// TODO@Ben migration
if (platform.isWindows || platform.isMacintosh) {
const legacyBackupResource = this.getBackupResource(resource, true /* legacyMacWindowsFormat */);
if (model.has(legacyBackupResource)) {
return this.getResourceIOQueue(legacyBackupResource).queue(() => {
return pfs.del(legacyBackupResource.fsPath).then(() => model.remove(legacyBackupResource));
});
}
}
return TPromise.as(void 0);
});
});
}
private getResourceIOQueue(resource: Uri) {
const key = resource.toString();
if (!this.ioOperationQueues[key]) {
const queue = new Queue<void>();
queue.onFinished(() => {
queue.dispose();
delete this.ioOperationQueues[key];
});
this.ioOperationQueues[key] = queue;
}
return this.ioOperationQueues[key];
}
public discardAllWorkspaceBackups(): TPromise<void> {
this.isShuttingDown = true;
return this.ready.then(model => {
if (!this.backupEnabled) {
return void 0;
}
return pfs.del(this.backupWorkspacePath).then(() => model.clear());
});
}
public getWorkspaceFileBackups(): TPromise<Uri[]> {
return this.ready.then(model => {
const readPromises: TPromise<Uri>[] = [];
model.get().forEach(fileBackup => {
readPromises.push(
readToMatchingString(fileBackup.fsPath, BackupFileService.META_MARKER, 2000, 10000)
.then(Uri.parse)
);
});
return TPromise.join(readPromises);
});
}
public parseBackupContent(rawTextSource: IRawTextSource): string {
const textSource = TextSource.fromRawTextSource(rawTextSource, DefaultEndOfLine.LF);
return textSource.lines.slice(1).join(textSource.EOL); // The first line of a backup text file is the file name
}
protected getBackupResource(resource: Uri, legacyMacWindowsFormat?: boolean): Uri {
if (!this.backupEnabled) {
return null;
}
return Uri.file(path.join(this.backupWorkspacePath, resource.scheme, this.hashPath(resource, legacyMacWindowsFormat)));
}
private hashPath(resource: Uri, legacyMacWindowsFormat?: boolean): string {
const caseAwarePath = legacyMacWindowsFormat ? resource.fsPath.toLowerCase() : resource.fsPath;
return crypto.createHash('md5').update(caseAwarePath).digest('hex');
}
}

View File

@@ -0,0 +1,382 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import crypto = require('crypto');
import os = require('os');
import fs = require('fs');
import path = require('path');
import extfs = require('vs/base/node/extfs');
import pfs = require('vs/base/node/pfs');
import Uri from 'vs/base/common/uri';
import { BackupFileService, BackupFilesModel } from 'vs/workbench/services/backup/node/backupFileService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs } from 'vs/platform/environment/node/argv';
import { RawTextSource } from 'vs/editor/common/model/textSource';
import { TestContextService } from 'vs/workbench/test/workbenchTestServices';
import { Workspace } from 'vs/platform/workspace/common/workspace';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
class TestEnvironmentService extends EnvironmentService {
constructor(private _backupHome: string, private _backupWorkspacesPath: string) {
super(parseArgs(process.argv), process.execPath);
}
get backupHome(): string { return this._backupHome; }
get backupWorkspacesPath(): string { return this._backupWorkspacesPath; }
}
const parentDir = path.join(os.tmpdir(), 'vsctests', 'service');
const backupHome = path.join(parentDir, 'Backups');
const workspacesJsonPath = path.join(backupHome, 'workspaces.json');
const workspaceResource = Uri.file(platform.isWindows ? 'c:\\workspace' : '/workspace');
const workspaceBackupPath = path.join(backupHome, crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex'));
const fooFile = Uri.file(platform.isWindows ? 'c:\\Foo' : '/Foo');
const barFile = Uri.file(platform.isWindows ? 'c:\\Bar' : '/Bar');
const untitledFile = Uri.from({ scheme: 'untitled', path: 'Untitled-1' });
const fooBackupPath = path.join(workspaceBackupPath, 'file', crypto.createHash('md5').update(fooFile.fsPath).digest('hex'));
const fooBackupPathLegacy = path.join(workspaceBackupPath, 'file', crypto.createHash('md5').update(fooFile.fsPath.toLowerCase()).digest('hex'));
const barBackupPath = path.join(workspaceBackupPath, 'file', crypto.createHash('md5').update(barFile.fsPath).digest('hex'));
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', crypto.createHash('md5').update(untitledFile.fsPath).digest('hex'));
class TestBackupFileService extends BackupFileService {
constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) {
const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, workspace.fsPath, [workspace])), new TestConfigurationService(), { disableWatcher: true });
super(workspaceBackupPath, fileService);
}
public getBackupResource(resource: Uri, legacyMacWindowsFormat?: boolean): Uri {
return super.getBackupResource(resource, legacyMacWindowsFormat);
}
}
suite('BackupFileService', () => {
let service: TestBackupFileService;
setup(done => {
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
// Delete any existing backups completely and then re-create it.
extfs.del(backupHome, os.tmpdir(), () => {
pfs.mkdirp(backupHome).then(() => {
pfs.writeFile(workspacesJsonPath, '').then(() => {
done();
});
});
});
});
teardown(done => {
extfs.del(backupHome, os.tmpdir(), done);
});
suite('getBackupResource', () => {
test('should get the correct backup path for text files', () => {
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePathHash>
const backupResource = fooFile;
const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex');
const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex');
const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'file', filePathHash)).fsPath;
assert.equal(service.getBackupResource(backupResource).fsPath, expectedPath);
});
test('should get the correct backup path for untitled files', () => {
// Format should be: <backupHome>/<workspaceHash>/<scheme>/<filePath>
const backupResource = Uri.from({ scheme: 'untitled', path: 'Untitled-1' });
const workspaceHash = crypto.createHash('md5').update(workspaceResource.fsPath).digest('hex');
const filePathHash = crypto.createHash('md5').update(backupResource.fsPath).digest('hex');
const expectedPath = Uri.file(path.join(backupHome, workspaceHash, 'untitled', filePathHash)).fsPath;
assert.equal(service.getBackupResource(backupResource).fsPath, expectedPath);
});
});
suite('loadBackupResource', () => {
test('should return whether a backup resource exists', done => {
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
service.loadBackupResource(fooFile).then(resource => {
assert.ok(resource);
assert.equal(path.basename(resource.fsPath), path.basename(fooBackupPath));
return service.hasBackups().then(hasBackups => {
assert.ok(hasBackups);
done();
});
});
});
});
test('should return whether a backup resource exists - legacy support (read old lowercase format as fallback)', done => {
if (platform.isLinux) {
done();
return; // only on mac and windows
}
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPathLegacy, 'foo');
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
service.loadBackupResource(fooFile).then(resource => {
assert.ok(resource);
assert.equal(path.basename(resource.fsPath), path.basename(fooBackupPathLegacy));
return service.hasBackups().then(hasBackups => {
assert.ok(hasBackups);
done();
});
});
});
});
test('should return whether a backup resource exists - legacy support #2 (both cases present, return case sensitive backup)', done => {
if (platform.isLinux) {
done();
return; // only on mac and windows
}
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
fs.writeFileSync(fooBackupPathLegacy, 'foo');
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
service.loadBackupResource(fooFile).then(resource => {
assert.ok(resource);
assert.equal(path.basename(resource.fsPath), path.basename(fooBackupPath));
return service.hasBackups().then(hasBackups => {
assert.ok(hasBackups);
done();
});
});
});
});
});
suite('backupResource', () => {
test('text file', function (done: () => void) {
service.backupResource(fooFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
assert.equal(fs.existsSync(fooBackupPath), true);
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`);
done();
});
});
test('untitled file', function (done: () => void) {
service.backupResource(untitledFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
assert.equal(fs.existsSync(untitledBackupPath), true);
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`);
done();
});
});
});
suite('discardResourceBackup', () => {
test('text file', function (done: () => void) {
service.backupResource(fooFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
service.discardResourceBackup(fooFile).then(() => {
assert.equal(fs.existsSync(fooBackupPath), false);
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
done();
});
});
});
test('untitled file', function (done: () => void) {
service.backupResource(untitledFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
service.discardResourceBackup(untitledFile).then(() => {
assert.equal(fs.existsSync(untitledBackupPath), false);
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 0);
done();
});
});
});
test('text file - legacy support (dicard lowercase backup file if present)', done => {
if (platform.isLinux) {
done();
return; // only on mac and windows
}
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPathLegacy, 'foo');
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
service.backupResource(fooFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
service.discardResourceBackup(fooFile).then(() => {
assert.equal(fs.existsSync(fooBackupPath), false);
assert.equal(fs.existsSync(fooBackupPathLegacy), false);
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 0);
done();
});
});
});
});
});
suite('discardAllWorkspaceBackups', () => {
test('text file', function (done: () => void) {
service.backupResource(fooFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
service.backupResource(barFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
service.discardAllWorkspaceBackups().then(() => {
assert.equal(fs.existsSync(fooBackupPath), false);
assert.equal(fs.existsSync(barBackupPath), false);
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'file')), false);
done();
});
});
});
});
test('untitled file', function (done: () => void) {
service.backupResource(untitledFile, 'test').then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
service.discardAllWorkspaceBackups().then(() => {
assert.equal(fs.existsSync(untitledBackupPath), false);
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'untitled')), false);
done();
});
});
});
test('should disable further backups', function (done: () => void) {
service.discardAllWorkspaceBackups().then(() => {
service.backupResource(untitledFile, 'test').then(() => {
assert.equal(fs.existsSync(workspaceBackupPath), false);
done();
});
});
});
});
suite('getWorkspaceFileBackups', () => {
test('("file") - text file', done => {
service.backupResource(fooFile, `test`).then(() => {
service.getWorkspaceFileBackups().then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath]);
service.backupResource(barFile, `test`).then(() => {
service.getWorkspaceFileBackups().then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath, barFile.fsPath]);
done();
});
});
});
});
});
test('("file") - untitled file', done => {
service.backupResource(untitledFile, `test`).then(() => {
service.getWorkspaceFileBackups().then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), [untitledFile.fsPath]);
done();
});
});
});
test('("untitled") - untitled file', done => {
service.backupResource(untitledFile, `test`).then(() => {
service.getWorkspaceFileBackups().then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), ['Untitled-1']);
done();
});
});
});
});
test('parseBackupContent', () => {
test('should separate metadata from content', () => {
const textSource = RawTextSource.fromString('metadata\ncontent');
assert.equal(service.parseBackupContent(textSource), 'content');
});
});
});
suite('BackupFilesModel', () => {
test('simple', () => {
const model = new BackupFilesModel();
const resource1 = Uri.file('test.html');
assert.equal(model.has(resource1), false);
model.add(resource1);
assert.equal(model.has(resource1), true);
assert.equal(model.has(resource1, 0), true);
assert.equal(model.has(resource1, 1), false);
model.remove(resource1);
assert.equal(model.has(resource1), false);
model.add(resource1);
assert.equal(model.has(resource1), true);
assert.equal(model.has(resource1, 0), true);
assert.equal(model.has(resource1, 1), false);
model.clear();
assert.equal(model.has(resource1), false);
model.add(resource1, 1);
assert.equal(model.has(resource1), true);
assert.equal(model.has(resource1, 0), false);
assert.equal(model.has(resource1, 1), true);
const resource2 = Uri.file('test1.html');
const resource3 = Uri.file('test2.html');
const resource4 = Uri.file('test3.html');
model.add(resource2);
model.add(resource3);
model.add(resource4);
assert.equal(model.has(resource1), true);
assert.equal(model.has(resource2), true);
assert.equal(model.has(resource3), true);
assert.equal(model.has(resource4), true);
});
test('resolve', (done) => {
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
const model = new BackupFilesModel();
model.resolve(workspaceBackupPath).then(model => {
assert.equal(model.has(Uri.file(fooBackupPath)), true);
done();
});
});
});
test('get', () => {
const model = new BackupFilesModel();
assert.deepEqual(model.get(), []);
const file1 = Uri.file('/root/file/foo.html');
const file2 = Uri.file('/root/file/bar.html');
const untitled = Uri.file('/root/untitled/bar.html');
model.add(file1);
model.add(file2);
model.add(untitled);
assert.deepEqual(model.get().map(f => f.fsPath), [file1.fsPath, file2.fsPath, untitled.fsPath]);
});
});

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const CONFIG_DEFAULT_NAME = 'settings';
// {{SQL CARBON EDIT}}
export const WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME = '.sqlops';
export const WORKSPACE_CONFIG_DEFAULT_PATH = `${WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME}/${CONFIG_DEFAULT_NAME}.json`;
export const IWorkspaceConfigurationService = createDecorator<IWorkspaceConfigurationService>('configurationService');
export interface IWorkspaceConfigurationService extends IConfigurationService {
/**
* Returns untrusted configuration keys for the current workspace.
*/
getUnsupportedWorkspaceKeys(): string[];
}
export const WORKSPACE_STANDALONE_CONFIGURATIONS = {
'tasks': `${WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME}/tasks.json`,
'launch': `${WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME}/launch.json`
};

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
export const IConfigurationEditingService = createDecorator<IConfigurationEditingService>('configurationEditingService');
export enum ConfigurationEditingErrorCode {
/**
* Error when trying to write a configuration key that is not registered.
*/
ERROR_UNKNOWN_KEY,
/**
* Error when trying to write an invalid folder configuration key to folder settings.
*/
ERROR_INVALID_FOLDER_CONFIGURATION,
/**
* Error when trying to write to user target but not supported for provided key.
*/
ERROR_INVALID_USER_TARGET,
/**
* Error when trying to write a configuration key to folder target
*/
ERROR_INVALID_FOLDER_TARGET,
/**
* Error when trying to write to the workspace configuration without having a workspace opened.
*/
ERROR_NO_WORKSPACE_OPENED,
/**
* Error when trying to write and save to the configuration file while it is dirty in the editor.
*/
ERROR_CONFIGURATION_FILE_DIRTY,
/**
* Error when trying to write to a configuration file that contains JSON errors.
*/
ERROR_INVALID_CONFIGURATION
}
export class ConfigurationEditingError extends Error {
constructor(message: string, public code: ConfigurationEditingErrorCode) {
super(message);
}
}
export enum ConfigurationTarget {
/**
* Targets the user configuration file for writing.
*/
USER,
/**
* Targets the workspace configuration file for writing. This only works if a workspace is opened.
*/
WORKSPACE,
/**
* Targets the folder configuration file for writing. This only works if a workspace is opened.
*/
FOLDER
}
export interface IConfigurationValue {
key: string;
value: any;
}
export interface IConfigurationEditingOptions {
/**
* If `true`, do not saves the configuration. Default is `false`.
*/
donotSave?: boolean;
/**
* If `true`, do not notifies the error to user by showing the message box. Default is `false`.
*/
donotNotifyError?: boolean;
/**
* Scope of configuration to be written into.
*/
scopes?: IConfigurationOverrides;
}
export interface IConfigurationEditingService {
_serviceBrand: ServiceIdentifier<any>;
/**
* Allows to write the configuration value to either the user or workspace configuration file and save it if asked to save.
* The returned promise will be in error state in any of the error cases from [ConfigurationEditingErrorCode](#ConfigurationEditingErrorCode)
*/
writeConfiguration(target: ConfigurationTarget, value: IConfigurationValue, options?: IConfigurationEditingOptions): TPromise<void>;
}

View File

@@ -0,0 +1,181 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { clone } from 'vs/base/common/objects';
import { CustomConfigurationModel, toValuesTree } from 'vs/platform/configuration/common/model';
import { ConfigurationModel } from 'vs/platform/configuration/common/configuration';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration';
import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
export class WorkspaceConfigurationModel<T> extends CustomConfigurationModel<T> {
private _raw: T;
private _folders: IStoredWorkspaceFolder[];
private _worksapaceSettings: ConfigurationModel<T>;
private _tasksConfiguration: ConfigurationModel<T>;
private _launchConfiguration: ConfigurationModel<T>;
private _workspaceConfiguration: ConfigurationModel<T>;
public update(content: string): void {
super.update(content);
this._worksapaceSettings = new ConfigurationModel(this._worksapaceSettings.contents, this._worksapaceSettings.keys, this.overrides);
this._workspaceConfiguration = this.consolidate();
}
get folders(): IStoredWorkspaceFolder[] {
return this._folders;
}
get workspaceConfiguration(): ConfigurationModel<T> {
return this._workspaceConfiguration;
}
protected processRaw(raw: T): void {
this._raw = raw;
this._folders = (this._raw['folders'] || []) as IStoredWorkspaceFolder[];
this._worksapaceSettings = this.parseConfigurationModel('settings');
this._tasksConfiguration = this.parseConfigurationModel('tasks');
this._launchConfiguration = this.parseConfigurationModel('launch');
super.processRaw(raw);
}
private parseConfigurationModel(section: string): ConfigurationModel<T> {
const rawSection = this._raw[section] || {};
const contents = toValuesTree(rawSection, message => console.error(`Conflict in section '${section}' of workspace configuration file ${message}`));
return new ConfigurationModel<T>(contents, Object.keys(rawSection));
}
private consolidate(): ConfigurationModel<T> {
const keys: string[] = [...this._worksapaceSettings.keys,
...this._tasksConfiguration.keys.map(key => `tasks.${key}`),
...this._launchConfiguration.keys.map(key => `launch.${key}`)];
const mergedContents = new ConfigurationModel<T>(<T>{}, keys)
.merge(this._worksapaceSettings)
.merge(this._tasksConfiguration)
.merge(this._launchConfiguration);
return new ConfigurationModel<T>(mergedContents.contents, keys, mergedContents.overrides);
}
}
export class ScopedConfigurationModel<T> extends CustomConfigurationModel<T> {
constructor(content: string, name: string, public readonly scope: string) {
super(null, name);
this.update(content);
}
public update(content: string): void {
super.update(content);
const contents = Object.create(null);
contents[this.scope] = this.contents;
this._contents = contents;
}
}
export class FolderSettingsModel<T> extends CustomConfigurationModel<T> {
private _raw: T;
private _unsupportedKeys: string[];
protected processRaw(raw: T): void {
this._raw = raw;
const processedRaw = <T>{};
this._unsupportedKeys = [];
const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
for (let key in raw) {
if (this.isNotExecutable(key, configurationProperties)) {
processedRaw[key] = raw[key];
} else {
this._unsupportedKeys.push(key);
}
}
return super.processRaw(processedRaw);
}
public reprocess(): void {
this.processRaw(this._raw);
}
public get unsupportedKeys(): string[] {
return this._unsupportedKeys || [];
}
private isNotExecutable(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): boolean {
const propertySchema = configurationProperties[key];
if (!propertySchema) {
return true; // Unknown propertis are ignored from checks
}
return !propertySchema.isExecutable;
}
public createWorkspaceConfigurationModel(): ConfigurationModel<any> {
return this.createScopedConfigurationModel(ConfigurationScope.WINDOW);
}
public createFolderScopedConfigurationModel(): ConfigurationModel<any> {
return this.createScopedConfigurationModel(ConfigurationScope.RESOURCE);
}
private createScopedConfigurationModel(scope: ConfigurationScope): ConfigurationModel<any> {
const workspaceRaw = <T>{};
const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
for (let key in this._raw) {
if (this.getScope(key, configurationProperties) === scope) {
workspaceRaw[key] = this._raw[key];
}
}
const workspaceContents = toValuesTree(workspaceRaw, message => console.error(`Conflict in workspace settings file: ${message}`));
const workspaceKeys = Object.keys(workspaceRaw);
return new ConfigurationModel(workspaceContents, workspaceKeys, clone(this._overrides));
}
private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope {
const propertySchema = configurationProperties[key];
return propertySchema ? propertySchema.scope : ConfigurationScope.WINDOW;
}
}
export class FolderConfigurationModel<T> extends CustomConfigurationModel<T> {
constructor(public readonly workspaceSettingsConfig: FolderSettingsModel<T>, private scopedConfigs: ScopedConfigurationModel<T>[], private scope: ConfigurationScope) {
super();
this.consolidate();
}
private consolidate(): void {
this._contents = <T>{};
this._overrides = [];
this.doMerge(this, ConfigurationScope.WINDOW === this.scope ? this.workspaceSettingsConfig : this.workspaceSettingsConfig.createFolderScopedConfigurationModel());
for (const configModel of this.scopedConfigs) {
this.doMerge(this, configModel);
}
}
public get keys(): string[] {
const keys: string[] = [...this.workspaceSettingsConfig.keys];
this.scopedConfigs.forEach(scopedConfigModel => {
Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS).forEach(scope => {
if (scopedConfigModel.scope === scope) {
keys.push(...scopedConfigModel.keys.map(key => `${scope}.${key}`));
}
});
});
return keys;
}
public update(): void {
this.workspaceSettingsConfig.reprocess();
this.consolidate();
}
}

View File

@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
export const IJSONEditingService = createDecorator<IJSONEditingService>('jsonEditingService');
export enum JSONEditingErrorCode {
/**
* Error when trying to write and save to the file while it is dirty in the editor.
*/
ERROR_FILE_DIRTY,
/**
* Error when trying to write to a file that contains JSON errors.
*/
ERROR_INVALID_FILE
}
export class JSONEditingError extends Error {
constructor(message: string, public code: JSONEditingErrorCode) {
super(message);
}
}
export interface IJSONValue {
key: string;
value: any;
}
export interface IJSONEditingService {
_serviceBrand: ServiceIdentifier<any>;
write(resource: URI, value: IJSONValue, save: boolean): TPromise<void>;
}

View File

@@ -0,0 +1,916 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import URI from 'vs/base/common/uri';
import * as paths from 'vs/base/common/paths';
import { TPromise } from 'vs/base/common/winjs.base';
import Event, { Emitter } from 'vs/base/common/event';
import { StrictResourceMap } from 'vs/base/common/map';
import { equals, coalesce } from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import * as errors from 'vs/base/common/errors';
import * as collections from 'vs/base/common/collections';
import { Disposable, toDisposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { readFile, stat } from 'vs/base/node/pfs';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import * as extfs from 'vs/base/node/extfs';
import { IWorkspaceContextService, IWorkspace, Workspace, ILegacyWorkspace, LegacyWorkspace } from 'vs/platform/workspace/common/workspace';
import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
import { ConfigWatcher } from 'vs/base/node/config';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { CustomConfigurationModel } from 'vs/platform/configuration/common/model';
import { WorkspaceConfigurationModel, ScopedConfigurationModel, FolderConfigurationModel, FolderSettingsModel } from 'vs/workbench/services/configuration/common/configurationModels';
import { IConfigurationServiceEvent, ConfigurationSource, IConfigurationKeys, IConfigurationValue, ConfigurationModel, IConfigurationOverrides, Configuration as BaseConfiguration, IConfigurationValues, IConfigurationData } from 'vs/platform/configuration/common/configuration';
import { IWorkspaceConfigurationService, WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME, WORKSPACE_STANDALONE_CONFIGURATIONS, WORKSPACE_CONFIG_DEFAULT_PATH } from 'vs/workbench/services/configuration/common/configuration';
import { ConfigurationService as GlobalConfigurationService } from 'vs/platform/configuration/node/configurationService';
import * as nls from 'vs/nls';
import { Registry } from 'vs/platform/registry/common/platform';
import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/platform/extensions/common/extensionsRegistry';
import { IConfigurationNode, IConfigurationRegistry, Extensions, editorConfigurationSchemaId, IDefaultConfigurationExtension, validateProperty, ConfigurationScope, schemaId } from 'vs/platform/configuration/common/configurationRegistry';
import { createHash } from 'crypto';
import { getWorkspaceLabel, IWorkspacesService, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
interface IStat {
resource: URI;
isDirectory?: boolean;
children?: { resource: URI; }[];
}
interface IContent {
resource: URI;
value: string;
}
interface IWorkspaceConfiguration<T> {
workspace: T;
consolidated: any;
}
type IWorkspaceFoldersConfiguration = { [rootFolder: string]: { folders: string[]; } };
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
// BEGIN VSCode extension point `configuration`
const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>('configuration', [], {
description: nls.localize('vscode.extension.contributes.configuration', 'Contributes configuration settings.'),
type: 'object',
defaultSnippets: [{ body: { title: '', properties: {} } }],
properties: {
title: {
description: nls.localize('vscode.extension.contributes.configuration.title', 'A summary of the settings. This label will be used in the settings file as separating comment.'),
type: 'string'
},
properties: {
description: nls.localize('vscode.extension.contributes.configuration.properties', 'Description of the configuration properties.'),
type: 'object',
additionalProperties: {
anyOf: [
{ $ref: 'http://json-schema.org/draft-04/schema#' },
{
type: 'object',
properties: {
isExecutable: {
type: 'boolean'
},
scope: {
type: 'string',
enum: ['window', 'resource'],
default: 'window',
enumDescriptions: [
nls.localize('scope.window.description', "Window specific configuration, which can be configured in the User or Workspace settings."),
nls.localize('scope.resource.description', "Resource specific configuration, which can be configured in the User, Workspace or Folder settings.")
],
description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `window` and `resource`.")
}
}
}
]
}
}
}
});
configurationExtPoint.setHandler(extensions => {
const configurations: IConfigurationNode[] = [];
for (let i = 0; i < extensions.length; i++) {
const configuration = <IConfigurationNode>objects.clone(extensions[i].value);
const collector = extensions[i].collector;
if (configuration.type && configuration.type !== 'object') {
collector.warn(nls.localize('invalid.type', "if set, 'configuration.type' must be set to 'object"));
} else {
configuration.type = 'object';
}
if (configuration.title && (typeof configuration.title !== 'string')) {
collector.error(nls.localize('invalid.title', "'configuration.title' must be a string"));
}
validateProperties(configuration, collector);
configuration.id = extensions[i].description.id;
configurations.push(configuration);
}
configurationRegistry.registerConfigurations(configurations, false);
});
// END VSCode extension point `configuration`
// BEGIN VSCode extension point `configurationDefaults`
const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>('configurationDefaults', [], {
description: nls.localize('vscode.extension.contributes.defaultConfiguration', 'Contributes default editor configuration settings by language.'),
type: 'object',
defaultSnippets: [{ body: {} }],
patternProperties: {
'\\[.*\\]$': {
type: 'object',
default: {},
$ref: editorConfigurationSchemaId,
}
}
});
defaultConfigurationExtPoint.setHandler(extensions => {
const defaultConfigurations: IDefaultConfigurationExtension[] = extensions.map(extension => {
const id = extension.description.id;
const name = extension.description.name;
const defaults = objects.clone(extension.value);
return <IDefaultConfigurationExtension>{
id, name, defaults
};
});
configurationRegistry.registerDefaultConfigurations(defaultConfigurations);
});
// END VSCode extension point `configurationDefaults`
function validateProperties(configuration: IConfigurationNode, collector: ExtensionMessageCollector): void {
let properties = configuration.properties;
if (properties) {
if (typeof properties !== 'object') {
collector.error(nls.localize('invalid.properties', "'configuration.properties' must be an object"));
configuration.properties = {};
}
for (let key in properties) {
const message = validateProperty(key);
const propertyConfiguration = configuration.properties[key];
propertyConfiguration.scope = propertyConfiguration.scope && propertyConfiguration.scope.toString() === 'resource' ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW;
if (message) {
collector.warn(message);
delete properties[key];
}
}
}
let subNodes = configuration.allOf;
if (subNodes) {
for (let node of subNodes) {
validateProperties(node, collector);
}
}
}
export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService {
public _serviceBrand: any;
protected workspace: Workspace = null;
protected legacyWorkspace: LegacyWorkspace = null;
protected _configuration: Configuration<any>;
protected readonly _onDidUpdateConfiguration: Emitter<IConfigurationServiceEvent> = this._register(new Emitter<IConfigurationServiceEvent>());
public readonly onDidUpdateConfiguration: Event<IConfigurationServiceEvent> = this._onDidUpdateConfiguration.event;
protected readonly _onDidChangeWorkspaceRoots: Emitter<void> = this._register(new Emitter<void>());
public readonly onDidChangeWorkspaceRoots: Event<void> = this._onDidChangeWorkspaceRoots.event;
protected readonly _onDidChangeWorkspaceName: Emitter<void> = this._register(new Emitter<void>());
public readonly onDidChangeWorkspaceName: Event<void> = this._onDidChangeWorkspaceName.event;
constructor() {
super();
this._configuration = new Configuration(new BaseConfiguration(new ConfigurationModel<any>(), new ConfigurationModel<any>()), new ConfigurationModel<any>(), new StrictResourceMap<FolderConfigurationModel<any>>(), this.workspace);
}
public getLegacyWorkspace(): ILegacyWorkspace {
return this.legacyWorkspace;
}
public getWorkspace(): IWorkspace {
return this.workspace;
}
public hasWorkspace(): boolean {
return !!this.workspace;
}
public hasFolderWorkspace(): boolean {
return this.workspace && !this.workspace.configuration;
}
public hasMultiFolderWorkspace(): boolean {
return this.workspace && !!this.workspace.configuration;
}
public getRoot(resource: URI): URI {
return this.workspace ? this.workspace.getRoot(resource) : null;
}
private get workspaceUri(): URI {
return this.workspace ? this.workspace.roots[0] : null;
}
public isInsideWorkspace(resource: URI): boolean {
return !!this.getRoot(resource);
}
public toResource(workspaceRelativePath: string): URI {
return this.workspace ? this.legacyWorkspace.toResource(workspaceRelativePath) : null;
}
public initialize(trigger: boolean = true): TPromise<any> {
this.resetCaches();
return this.updateConfiguration()
.then(() => {
if (trigger) {
this.triggerConfigurationChange();
}
});
}
public reloadConfiguration(section?: string): TPromise<any> {
return TPromise.as(this.getConfiguration(section));
}
public getConfigurationData<T>(): IConfigurationData<T> {
return this._configuration.toData();
}
public getConfiguration<C>(section?: string, overrides?: IConfigurationOverrides): C {
return this._configuration.getValue<C>(section, overrides);
}
public lookup<C>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<C> {
return this._configuration.lookup<C>(key, overrides);
}
public keys(overrides?: IConfigurationOverrides): IConfigurationKeys {
return this._configuration.keys(overrides);
}
public values<V>(): IConfigurationValues {
return this._configuration.values();
}
public getUnsupportedWorkspaceKeys(): string[] {
return [];
}
public isInWorkspaceContext(): boolean {
return false;
}
protected triggerConfigurationChange(): void {
this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.Workspace, sourceConfig: void 0 });
}
public handleWorkspaceFileEvents(event: FileChangesEvent): void {
// implemented by sub classes
}
protected resetCaches(): void {
// implemented by sub classes
}
protected updateConfiguration(): TPromise<boolean> {
// implemented by sub classes
return TPromise.as(false);
}
}
export class EmptyWorkspaceServiceImpl extends WorkspaceService {
private baseConfigurationService: GlobalConfigurationService<any>;
constructor(environmentService: IEnvironmentService) {
super();
this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
this.resetCaches();
}
public reloadConfiguration(section?: string): TPromise<any> {
const current = this._configuration;
return this.baseConfigurationService.reloadConfiguration()
.then(() => this.initialize(false)) // Reinitialize to ensure we are hitting the disk
.then(() => {
// Check and trigger
if (!this._configuration.equals(current)) {
this.triggerConfigurationChange();
}
return super.reloadConfiguration(section);
});
}
private onBaseConfigurationChanged({ source, sourceConfig }: IConfigurationServiceEvent): void {
if (this._configuration.updateBaseConfiguration(<any>this.baseConfigurationService.configuration())) {
this._onDidUpdateConfiguration.fire({ source, sourceConfig });
}
}
protected resetCaches(): void {
this._configuration = new Configuration(<any>this.baseConfigurationService.configuration(), new ConfigurationModel<any>(), new StrictResourceMap<FolderConfigurationModel<any>>(), null);
}
protected triggerConfigurationChange(): void {
this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.User, sourceConfig: this._configuration.user.contents });
}
}
export class WorkspaceServiceImpl extends WorkspaceService {
public _serviceBrand: any;
private workspaceConfigPath: URI;
private folderPath: URI;
private baseConfigurationService: GlobalConfigurationService<any>;
private workspaceConfiguration: WorkspaceConfiguration;
private cachedFolderConfigs: StrictResourceMap<FolderConfiguration<any>>;
constructor(private workspaceIdentifier: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier, private environmentService: IEnvironmentService, private workspacesService: IWorkspacesService, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME) {
super();
if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier)) {
this.folderPath = URI.file(workspaceIdentifier);
} else {
this.workspaceConfigPath = URI.file(workspaceIdentifier.configPath);
}
this.workspaceConfiguration = this._register(new WorkspaceConfiguration());
this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService));
}
public getUnsupportedWorkspaceKeys(): string[] {
return this.hasFolderWorkspace() ? this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).workspaceSettingsConfig.unsupportedKeys : [];
}
public initialize(trigger: boolean = true): TPromise<any> {
if (!this.workspace) {
return this.initializeWorkspace()
.then(() => super.initialize(trigger));
}
if (this.hasMultiFolderWorkspace()) {
return this.workspaceConfiguration.load(this.workspaceConfigPath)
.then(() => super.initialize(trigger));
}
return super.initialize(trigger);
}
public reloadConfiguration(section?: string): TPromise<any> {
const current = this._configuration;
return this.baseConfigurationService.reloadConfiguration()
.then(() => this.initialize(false)) // Reinitialize to ensure we are hitting the disk
.then(() => {
// Check and trigger
if (!this._configuration.equals(current)) {
this.triggerConfigurationChange();
}
return super.reloadConfiguration(section);
});
}
public handleWorkspaceFileEvents(event: FileChangesEvent): void {
TPromise.join(this.workspace.roots.map(folder => this.cachedFolderConfigs.get(folder).handleWorkspaceFileEvents(event))) // handle file event for each folder
.then(folderConfigurations =>
folderConfigurations.map((configuration, index) => ({ configuration, folder: this.workspace.roots[index] }))
.filter(folderConfiguration => !!folderConfiguration.configuration) // Filter folders which are not impacted by events
.map(folderConfiguration => this.updateFolderConfiguration(folderConfiguration.folder, folderConfiguration.configuration, true)) // Update the configuration of impacted folders
.reduce((result, value) => result || value, false)) // Check if the effective configuration of folder is changed
.then(changed => changed ? this.triggerConfigurationChange() : void 0); // Trigger event if changed
}
protected resetCaches(): void {
this.cachedFolderConfigs = new StrictResourceMap<FolderConfiguration<any>>();
this._configuration = new Configuration(<any>this.baseConfigurationService.configuration(), new ConfigurationModel<any>(), new StrictResourceMap<FolderConfigurationModel<any>>(), this.workspace);
this.initCachesForFolders(this.workspace.roots);
}
private initializeWorkspace(): TPromise<void> {
return (this.workspaceConfigPath ? this.initializeMulitFolderWorkspace() : this.initializeSingleFolderWorkspace())
.then(() => {
this._register(this.baseConfigurationService.onDidUpdateConfiguration(e => this.onBaseConfigurationChanged(e)));
});
}
// TODO@Sandeep use again once we can change workspace without window reload
// private onWorkspaceChange(configPath: URI): TPromise<void> {
// let workspaceName = this.workspace.name;
// this.workspaceConfigPath = configPath;
// // Reset the workspace if current workspace is single folder
// if (this.hasFolderWorkspace()) {
// this.folderPath = null;
// this.workspace = null;
// }
// // Update workspace configuration path with new path
// else {
// this.workspace.configuration = configPath;
// this.workspace.name = getWorkspaceLabel({ id: this.workspace.id, configPath: this.workspace.configuration.fsPath }, this.environmentService);
// }
// return this.initialize().then(() => {
// if (workspaceName !== this.workspace.name) {
// this._onDidChangeWorkspaceName.fire();
// }
// });
// }
private initializeMulitFolderWorkspace(): TPromise<void> {
this.registerWorkspaceConfigSchema();
return this.workspaceConfiguration.load(this.workspaceConfigPath)
.then(() => {
const workspaceConfigurationModel = this.workspaceConfiguration.workspaceConfigurationModel;
const workspaceFolders = this.parseWorkspaceFolders(workspaceConfigurationModel.folders);
if (!workspaceFolders.length) {
return TPromise.wrapError<void>(new Error('Invalid workspace configuraton file ' + this.workspaceConfigPath));
}
const workspaceId = (this.workspaceIdentifier as IWorkspaceIdentifier).id;
const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: this.workspaceConfigPath.fsPath }, this.environmentService);
this.workspace = new Workspace(workspaceId, workspaceName, workspaceFolders, this.workspaceConfigPath);
this.legacyWorkspace = new LegacyWorkspace(this.workspace.roots[0]);
this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged()));
return null;
});
}
private parseWorkspaceFolders(configuredFolders: IStoredWorkspaceFolder[]): URI[] {
return coalesce(configuredFolders.map(configuredFolder => {
const path = configuredFolder.path;
if (!path) {
return void 0;
}
if (paths.isAbsolute(path)) {
return URI.file(path);
}
return URI.file(paths.join(paths.dirname(this.workspaceConfigPath.fsPath), path));
}));
}
private registerWorkspaceConfigSchema(): void {
const contributionRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
if (!contributionRegistry.getSchemaContributions().schemas['vscode://schemas/workspaceConfig']) {
contributionRegistry.registerSchema('vscode://schemas/workspaceConfig', {
default: {
folders: [
{
path: ''
}
],
settings: {
}
},
required: ['folders'],
properties: {
'folders': {
minItems: 1,
uniqueItems: true,
description: nls.localize('workspaceConfig.folders.description', "List of folders to be loaded in the workspace. Must be a file path. e.g. `/root/folderA` or `./folderA` for a relative path that will be resolved against the location of the workspace file."),
items: {
type: 'object',
default: { path: '' },
properties: {
path: {
type: 'string',
description: nls.localize('workspaceConfig.folder.description', "A file path. e.g. `/root/folderA` or `./folderA` for a relative path that will be resolved against the location of the workspace file.")
}
}
}
},
'settings': {
type: 'object',
default: {},
description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
$ref: schemaId
}
}
});
}
}
private initializeSingleFolderWorkspace(): TPromise<void> {
return stat(this.folderPath.fsPath)
.then(workspaceStat => {
const ctime = isLinux ? workspaceStat.ino : workspaceStat.birthtime.getTime(); // On Linux, birthtime is ctime, so we cannot use it! We use the ino instead!
const id = createHash('md5').update(this.folderPath.fsPath).update(ctime ? String(ctime) : '').digest('hex');
const folder = URI.file(this.folderPath.fsPath);
this.workspace = new Workspace(id, paths.basename(this.folderPath.fsPath), [folder], null);
this.legacyWorkspace = new LegacyWorkspace(folder, ctime);
return TPromise.as(null);
});
}
private initCachesForFolders(folders: URI[]): void {
for (const folder of folders) {
this.cachedFolderConfigs.set(folder, this._register(new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.hasMultiFolderWorkspace() ? ConfigurationScope.RESOURCE : ConfigurationScope.WINDOW)));
this.updateFolderConfiguration(folder, new FolderConfigurationModel<any>(new FolderSettingsModel<any>(null), [], ConfigurationScope.RESOURCE), false);
}
}
protected updateConfiguration(folders: URI[] = this.workspace.roots): TPromise<boolean> {
return TPromise.join([...folders.map(folder => this.cachedFolderConfigs.get(folder).loadConfiguration()
.then(configuration => this.updateFolderConfiguration(folder, configuration, true)))])
.then(changed => changed.reduce((result, value) => result || value, false))
.then(changed => this.updateWorkspaceConfiguration(true) || changed);
}
private onBaseConfigurationChanged({ source, sourceConfig }: IConfigurationServiceEvent): void {
if (source === ConfigurationSource.Default) {
this.workspace.roots.forEach(folder => this._configuration.getFolderConfigurationModel(folder).update());
}
if (this._configuration.updateBaseConfiguration(<any>this.baseConfigurationService.configuration())) {
this._onDidUpdateConfiguration.fire({ source, sourceConfig });
}
}
private onWorkspaceConfigurationChanged(): void {
let configuredFolders = this.parseWorkspaceFolders(this.workspaceConfiguration.workspaceConfigurationModel.folders);
const foldersChanged = !equals(this.workspace.roots, configuredFolders, (r1, r2) => r1.fsPath === r2.fsPath);
if (foldersChanged) { // TODO@Sandeep be smarter here about detecting changes
this.workspace.roots = configuredFolders;
this.onFoldersChanged()
.then(configurationChanged => {
this._onDidChangeWorkspaceRoots.fire();
if (configurationChanged) {
this.triggerConfigurationChange();
}
});
} else {
const configurationChanged = this.updateWorkspaceConfiguration(true);
if (configurationChanged) {
this.triggerConfigurationChange();
}
}
}
private onFoldersChanged(): TPromise<boolean> {
let configurationChangedOnRemoval = false;
// Remove the configurations of deleted folders
for (const key of this.cachedFolderConfigs.keys()) {
if (!this.workspace.roots.filter(folder => folder.toString() === key.toString())[0]) {
this.cachedFolderConfigs.delete(key);
if (this._configuration.deleteFolderConfiguration(key)) {
configurationChangedOnRemoval = true;
}
}
}
// Initialize the newly added folders
const toInitialize = this.workspace.roots.filter(folder => !this.cachedFolderConfigs.has(folder));
if (toInitialize.length) {
this.initCachesForFolders(toInitialize);
return this.updateConfiguration(toInitialize)
.then(changed => configurationChangedOnRemoval || changed);
} else if (configurationChangedOnRemoval) {
this.updateWorkspaceConfiguration(false);
return TPromise.as(true);
}
return TPromise.as(false);
}
private updateFolderConfiguration(folder: URI, folderConfiguration: FolderConfigurationModel<any>, compare: boolean): boolean {
let configurationChanged = this._configuration.updateFolderConfiguration(folder, folderConfiguration, compare);
if (this.hasFolderWorkspace()) {
// Workspace configuration changed
configurationChanged = this.updateWorkspaceConfiguration(compare) || configurationChanged;
}
return configurationChanged;
}
private updateWorkspaceConfiguration(compare: boolean): boolean {
const workspaceConfiguration = this.hasMultiFolderWorkspace() ? this.workspaceConfiguration.workspaceConfigurationModel.workspaceConfiguration : this._configuration.getFolderConfigurationModel(this.workspace.roots[0]);
return this._configuration.updateWorkspaceConfiguration(workspaceConfiguration, compare);
}
protected triggerConfigurationChange(): void {
this._onDidUpdateConfiguration.fire({ source: ConfigurationSource.Workspace, sourceConfig: this._configuration.getFolderConfigurationModel(this.workspace.roots[0]).contents });
}
}
class WorkspaceConfiguration extends Disposable {
private _workspaceConfigPath: URI;
private _workspaceConfigurationWatcher: ConfigWatcher<WorkspaceConfigurationModel<any>>;
private _workspaceConfigurationWatcherDisposables: IDisposable[] = [];
private _onDidUpdateConfiguration: Emitter<void> = this._register(new Emitter<void>());
public readonly onDidUpdateConfiguration: Event<void> = this._onDidUpdateConfiguration.event;
load(workspaceConfigPath: URI): TPromise<void> {
if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) {
return this._reload();
}
this._workspaceConfigPath = workspaceConfigPath;
this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables);
return new TPromise<void>((c, e) => {
this._workspaceConfigurationWatcher = new ConfigWatcher(this._workspaceConfigPath.fsPath, {
changeBufferDelay: 300, onError: error => errors.onUnexpectedError(error), defaultConfig: new WorkspaceConfigurationModel(null, this._workspaceConfigPath.fsPath), parse: (content: string, parseErrors: any[]) => {
const workspaceConfigurationModel = new WorkspaceConfigurationModel(content, this._workspaceConfigPath.fsPath);
parseErrors = [...workspaceConfigurationModel.errors];
return workspaceConfigurationModel;
}, initCallback: () => c(null)
});
this._workspaceConfigurationWatcherDisposables.push(toDisposable(() => this._workspaceConfigurationWatcher.dispose()));
this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire(), this, this._workspaceConfigurationWatcherDisposables);
});
}
get workspaceConfigurationModel(): WorkspaceConfigurationModel<any> {
return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModel();
}
private _reload(): TPromise<void> {
return new TPromise<void>(c => this._workspaceConfigurationWatcher.reload(() => c(null)));
}
dispose(): void {
dispose(this._workspaceConfigurationWatcherDisposables);
super.dispose();
}
}
class FolderConfiguration<T> extends Disposable {
private static RELOAD_CONFIGURATION_DELAY = 50;
private bulkFetchFromWorkspacePromise: TPromise<any>;
private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<ConfigurationModel<any>> };
private reloadConfigurationScheduler: RunOnceScheduler;
private reloadConfigurationEventEmitter: Emitter<FolderConfigurationModel<T>> = new Emitter<FolderConfigurationModel<T>>();
constructor(private folder: URI, private configFolderRelativePath: string, private scope: ConfigurationScope) {
super();
this.workspaceFilePathToConfiguration = Object.create(null);
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.loadConfiguration().then(configuration => this.reloadConfigurationEventEmitter.fire(configuration), errors.onUnexpectedError), FolderConfiguration.RELOAD_CONFIGURATION_DELAY));
}
loadConfiguration(): TPromise<FolderConfigurationModel<T>> {
// Load workspace locals
return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => {
// Consolidate (support *.json files in the workspace settings folder)
const workspaceSettingsConfig = <FolderSettingsModel<T>>workspaceConfigFiles[WORKSPACE_CONFIG_DEFAULT_PATH] || new FolderSettingsModel<T>(null);
const otherConfigModels = Object.keys(workspaceConfigFiles).filter(key => key !== WORKSPACE_CONFIG_DEFAULT_PATH).map(key => <ScopedConfigurationModel<T>>workspaceConfigFiles[key]);
return new FolderConfigurationModel<T>(workspaceSettingsConfig, otherConfigModels, this.scope);
});
}
private loadWorkspaceConfigFiles<T>(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModel<T> }> {
// once: when invoked for the first time we fetch json files that contribute settings
if (!this.bulkFetchFromWorkspacePromise) {
this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => {
if (!stat.isDirectory) {
return TPromise.as([]);
}
return resolveContents(stat.children.filter(stat => {
const isJson = paths.extname(stat.resource.fsPath) === '.json';
if (!isJson) {
return false; // only JSON files
}
return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files
}).map(stat => stat.resource));
}, err => [] /* never fail this call */)
.then((contents: IContent[]) => {
contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content)));
}, errors.onUnexpectedError);
}
// on change: join on *all* configuration file promises so that we can merge them into a single configuration object. this
// happens whenever a config file changes, is deleted, or added
return this.bulkFetchFromWorkspacePromise.then(() => TPromise.join(this.workspaceFilePathToConfiguration));
}
public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise<FolderConfigurationModel<T>> {
const events = event.changes;
let affectedByChanges = false;
// Find changes that affect workspace configuration files
for (let i = 0, len = events.length; i < len; i++) {
const resource = events[i].resource;
const isJson = paths.extname(resource.fsPath) === '.json';
const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.isEqual(paths.basename(resource.fsPath), this.configFolderRelativePath));
if (!isJson && !isDeletedSettingsFolder) {
continue; // only JSON files or the actual settings folder
}
const workspacePath = this.toFolderRelativePath(resource);
if (!workspacePath) {
continue; // event is not inside workspace
}
// Handle case where ".vscode" got deleted
if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) {
this.workspaceFilePathToConfiguration = Object.create(null);
affectedByChanges = true;
}
// only valid workspace config files
if (!this.isWorkspaceConfigurationFile(workspacePath)) {
continue;
}
// insert 'fetch-promises' for add and update events and
// remove promises for delete events
switch (events[i].type) {
case FileChangeType.DELETED:
affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, workspacePath);
break;
case FileChangeType.UPDATED:
case FileChangeType.ADDED:
this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => this.createConfigModel(content), errors.onUnexpectedError);
affectedByChanges = true;
}
}
if (!affectedByChanges) {
return TPromise.as(null);
}
return new TPromise((c, e) => {
let disposable = this.reloadConfigurationEventEmitter.event(configuration => {
disposable.dispose();
c(configuration);
});
// trigger reload of the configuration if we are affected by changes
if (!this.reloadConfigurationScheduler.isScheduled()) {
this.reloadConfigurationScheduler.schedule();
}
});
}
private createConfigModel<T>(content: IContent): ConfigurationModel<T> {
const path = this.toFolderRelativePath(content.resource);
if (path === WORKSPACE_CONFIG_DEFAULT_PATH) {
return new FolderSettingsModel<T>(content.value, content.resource.toString());
} else {
const matches = /\/([^\.]*)*\.json/.exec(path);
if (matches && matches[1]) {
return new ScopedConfigurationModel<T>(content.value, content.resource.toString(), matches[1]);
}
}
return new CustomConfigurationModel<T>(null);
}
private isWorkspaceConfigurationFile(folderRelativePath: string): boolean {
return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS.launch, WORKSPACE_STANDALONE_CONFIGURATIONS.tasks].some(p => p === folderRelativePath);
}
private toResource(folderRelativePath: string): URI {
if (typeof folderRelativePath === 'string') {
return URI.file(paths.join(this.folder.fsPath, folderRelativePath));
}
return null;
}
private toFolderRelativePath(resource: URI, toOSPath?: boolean): string {
if (this.contains(resource)) {
return paths.normalize(paths.relative(this.folder.fsPath, resource.fsPath), toOSPath);
}
return null;
}
private contains(resource: URI): boolean {
if (resource) {
return paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */);
}
return false;
}
}
// node.hs helper functions
function resolveContents(resources: URI[]): TPromise<IContent[]> {
const contents: IContent[] = [];
return TPromise.join(resources.map(resource => {
return resolveContent(resource).then(content => {
contents.push(content);
});
})).then(() => contents);
}
function resolveContent(resource: URI): TPromise<IContent> {
return readFile(resource.fsPath).then(contents => ({ resource, value: contents.toString() }));
}
function resolveStat(resource: URI): TPromise<IStat> {
return new TPromise<IStat>((c, e) => {
extfs.readdir(resource.fsPath, (error, children) => {
if (error) {
if ((<any>error).code === 'ENOTDIR') {
c({ resource });
} else {
e(error);
}
} else {
c({
resource,
isDirectory: true,
children: children.map(child => { return { resource: URI.file(paths.join(resource.fsPath, child)) }; })
});
}
});
});
}
export class Configuration<T> extends BaseConfiguration<T> {
constructor(private _baseConfiguration: BaseConfiguration<T>, workspaceConfiguration: ConfigurationModel<T>, protected folders: StrictResourceMap<FolderConfigurationModel<T>>, workspace: Workspace) {
super(_baseConfiguration.defaults, _baseConfiguration.user, workspaceConfiguration, folders, workspace);
}
updateBaseConfiguration(baseConfiguration: BaseConfiguration<T>): boolean {
const current = new Configuration(this._baseConfiguration, this._workspaceConfiguration, this.folders, this._workspace);
this._baseConfiguration = baseConfiguration;
this._defaults = this._baseConfiguration.defaults;
this._user = this._baseConfiguration.user;
this.merge();
return !this.equals(current);
}
updateWorkspaceConfiguration(workspaceConfiguration: ConfigurationModel<T>, compare: boolean = true): boolean {
const current = new Configuration(this._baseConfiguration, this._workspaceConfiguration, this.folders, this._workspace);
this._workspaceConfiguration = workspaceConfiguration;
this.merge();
return compare && !this.equals(current);
}
updateFolderConfiguration(resource: URI, configuration: FolderConfigurationModel<T>, compare: boolean): boolean {
const current = this.getValue(null, { resource });
this.folders.set(resource, configuration);
this.mergeFolder(resource);
return compare && !objects.equals(current, this.getValue(null, { resource }));
}
deleteFolderConfiguration(folder: URI): boolean {
if (this._workspace && this._workspace.roots.length > 0 && this._workspace.roots[0].fsPath === folder.fsPath) {
// Do not remove workspace configuration
return false;
}
const changed = this.folders.get(folder).keys.length > 0;
this.folders.delete(folder);
this._foldersConsolidatedConfigurations.delete(folder);
return changed;
}
getFolderConfigurationModel(folder: URI): FolderConfigurationModel<T> {
return <FolderConfigurationModel<T>>this.folders.get(folder);
}
equals(other: any): boolean {
if (!other || !(other instanceof Configuration)) {
return false;
}
if (!objects.equals(this.getValue(), other.getValue())) {
return false;
}
if (this._foldersConsolidatedConfigurations.size !== other._foldersConsolidatedConfigurations.size) {
return false;
}
for (const resource of this._foldersConsolidatedConfigurations.keys()) {
if (!objects.equals(this.getValue(null, { resource }), other.getValue(null, { resource }))) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,353 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import * as paths from 'vs/base/common/paths';
import URI from 'vs/base/common/uri';
import * as json from 'vs/base/common/json';
import * as encoding from 'vs/base/node/encoding';
import strings = require('vs/base/common/strings');
import { setProperty } from 'vs/base/common/jsonEdit';
import { Queue } from 'vs/base/common/async';
import { Edit } from 'vs/base/common/jsonFormatter';
import { IReference } from 'vs/base/common/lifecycle';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Registry } from 'vs/platform/registry/common/platform';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IConfigurationService, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
import { keyFromOverrideIdentifier } from 'vs/platform/configuration/common/model';
import { WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration';
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationEditingService, ConfigurationEditingErrorCode, ConfigurationEditingError, ConfigurationTarget, IConfigurationValue, IConfigurationEditingOptions } from 'vs/workbench/services/configuration/common/configurationEditing';
import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { OVERRIDE_PROPERTY_PATTERN, IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { IChoiceService, IMessageService, Severity } from 'vs/platform/message/common/message';
import { ICommandService } from 'vs/platform/commands/common/commands';
interface IConfigurationEditOperation extends IConfigurationValue {
jsonPath: json.JSONPath;
resource: URI;
isWorkspaceStandalone?: boolean;
}
interface IValidationResult {
error?: ConfigurationEditingErrorCode;
exists?: boolean;
}
interface ConfigurationEditingOptions extends IConfigurationEditingOptions {
force?: boolean;
}
export class ConfigurationEditingService implements IConfigurationEditingService {
public _serviceBrand: any;
private queue: Queue<void>;
constructor(
@IConfigurationService private configurationService: IConfigurationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IFileService private fileService: IFileService,
@ITextModelService private textModelResolverService: ITextModelService,
@ITextFileService private textFileService: ITextFileService,
@IChoiceService private choiceService: IChoiceService,
@IMessageService private messageService: IMessageService,
@ICommandService private commandService: ICommandService
) {
this.queue = new Queue<void>();
}
writeConfiguration(target: ConfigurationTarget, value: IConfigurationValue, options: IConfigurationEditingOptions = {}): TPromise<void> {
return this.queue.queue(() => this.doWriteConfiguration(target, value, options) // queue up writes to prevent race conditions
.then(() => null,
error => {
if (!options.donotNotifyError) {
this.onError(error, target, value, options.scopes);
}
return TPromise.wrapError(error);
}));
}
private doWriteConfiguration(target: ConfigurationTarget, value: IConfigurationValue, options: ConfigurationEditingOptions): TPromise<void> {
const operation = this.getConfigurationEditOperation(target, value, options.scopes || {});
const checkDirtyConfiguration = !(options.force || options.donotSave);
const saveConfiguration = options.force || !options.donotSave;
return this.resolveAndValidate(target, operation, checkDirtyConfiguration, options.scopes || {})
.then(reference => this.writeToBuffer(reference.object.textEditorModel, operation, saveConfiguration)
.then(() => reference.dispose()));
}
private writeToBuffer(model: editorCommon.IModel, operation: IConfigurationEditOperation, save: boolean): TPromise<any> {
const edit = this.getEdits(model, operation)[0];
if (this.applyEditsToBuffer(edit, model) && save) {
return this.textFileService.save(operation.resource, { skipSaveParticipants: true /* programmatic change */ })
// Reload the configuration so that we make sure all parties are updated
.then(() => this.configurationService.reloadConfiguration());
}
return TPromise.as(null);
}
private applyEditsToBuffer(edit: Edit, model: editorCommon.IModel): boolean {
const startPosition = model.getPositionAt(edit.offset);
const endPosition = model.getPositionAt(edit.offset + edit.length);
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
let currentText = model.getValueInRange(range);
if (edit.content !== currentText) {
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
return true;
}
return false;
}
private onError(error: ConfigurationEditingError, target: ConfigurationTarget, value: IConfigurationValue, scopes: IConfigurationOverrides): void {
switch (error.code) {
case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION:
this.onInvalidConfigurationError(error, target);
break;
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY:
this.onConfigurationFileDirtyError(error, target, value, scopes);
break;
default:
this.messageService.show(Severity.Error, error.message);
}
}
private onInvalidConfigurationError(error: ConfigurationEditingError, target: ConfigurationTarget): void {
this.choiceService.choose(Severity.Error, error.message, [nls.localize('open', "Open Settings"), nls.localize('close', "Close")], 1)
.then(option => {
switch (option) {
case 0:
this.openSettings(target);
}
});
}
private onConfigurationFileDirtyError(error: ConfigurationEditingError, target: ConfigurationTarget, value: IConfigurationValue, scopes: IConfigurationOverrides): void {
this.choiceService.choose(Severity.Error, error.message, [nls.localize('saveAndRetry', "Save Settings and Retry"), nls.localize('open', "Open Settings"), nls.localize('close', "Close")], 2)
.then(option => {
switch (option) {
case 0:
this.writeConfiguration(target, value, <ConfigurationEditingOptions>{ force: true, scopes });
break;
case 1:
this.openSettings(target);
break;
}
});
}
private openSettings(target: ConfigurationTarget): void {
this.commandService.executeCommand(ConfigurationTarget.USER === target ? 'workbench.action.openGlobalSettings' : 'workbench.action.openWorkspaceSettings');
}
private wrapError<T = never>(code: ConfigurationEditingErrorCode, target: ConfigurationTarget, operation: IConfigurationEditOperation): TPromise<T> {
const message = this.toErrorMessage(code, target, operation);
return TPromise.wrapError<T>(new ConfigurationEditingError(message, code));
}
private toErrorMessage(error: ConfigurationEditingErrorCode, target: ConfigurationTarget, operation: IConfigurationEditOperation): string {
switch (error) {
// API constraints
case ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY: return nls.localize('errorUnknownKey', "Unable to write to {0} because {1} is not a registered configuration.", this.stringifyTarget(target), operation.key);
case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION: return nls.localize('errorInvalidFolderConfiguration', "Unable to write to Folder Settings because {0} does not support the folder resource scope.", operation.key);
case ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET: return nls.localize('errorInvalidUserTarget', "Unable to write to User Settings because {0} does not support for global scope.", operation.key);
case ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET: return nls.localize('errorInvalidFolderTarget', "Unable to write to Folder Settings because no resource is provided.");
case ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED: return nls.localize('errorNoWorkspaceOpened', "Unable to write to {0} because no workspace is opened. Please open a workspace first and try again.", this.stringifyTarget(target));
// User issues
case ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION: {
if (target === ConfigurationTarget.USER) {
return nls.localize('errorInvalidConfiguration', "Unable to write into settings. Please open **User Settings** to correct errors/warnings in the file and try again.");
}
return nls.localize('errorInvalidConfigurationWorkspace', "Unable to write into settings. Please open **Workspace Settings** to correct errors/warnings in the file and try again.");
};
case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: {
if (target === ConfigurationTarget.USER) {
return nls.localize('errorConfigurationFileDirty', "Unable to write into settings because the file is dirty. Please save the **User Settings** file and try again.");
}
return nls.localize('errorConfigurationFileDirtyWorkspace', "Unable to write into settings because the file is dirty. Please save the **Workspace Settings** file and try again.");
};
}
}
private stringifyTarget(target: ConfigurationTarget): string {
switch (target) {
case ConfigurationTarget.USER:
return nls.localize('userTarget', "User Settings");
case ConfigurationTarget.WORKSPACE:
return nls.localize('workspaceTarget', "Workspace Settings");
case ConfigurationTarget.FOLDER:
return nls.localize('folderTarget', "Folder Settings");
}
}
private getEdits(model: editorCommon.IModel, edit: IConfigurationEditOperation): Edit[] {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const { value, jsonPath } = edit;
// Without jsonPath, the entire configuration file is being replaced, so we just use JSON.stringify
if (!jsonPath.length) {
const content = JSON.stringify(value, null, insertSpaces ? strings.repeat(' ', tabSize) : '\t');
return [{
content,
length: content.length,
offset: 0
}];
}
return setProperty(model.getValue(), jsonPath, value, { tabSize, insertSpaces, eol });
}
private resolveModelReference(resource: URI): TPromise<IReference<ITextEditorModel>> {
return this.fileService.existsFile(resource)
.then(exists => {
const result = exists ? TPromise.as(null) : this.fileService.updateContent(resource, '{}', { encoding: encoding.UTF8 });
return result.then(() => this.textModelResolverService.createModelReference(resource));
});
}
private hasParseErrors(model: editorCommon.IModel, operation: IConfigurationEditOperation): boolean {
// If we write to a workspace standalone file and replace the entire contents (no key provided)
// we can return here because any parse errors can safely be ignored since all contents are replaced
if (operation.isWorkspaceStandalone && !operation.key) {
return false;
}
const parseErrors: json.ParseError[] = [];
json.parse(model.getValue(), parseErrors, { allowTrailingComma: true });
return parseErrors.length > 0;
}
private resolveAndValidate(target: ConfigurationTarget, operation: IConfigurationEditOperation, checkDirty: boolean, overrides: IConfigurationOverrides): TPromise<IReference<ITextEditorModel>> {
// Any key must be a known setting from the registry (unless this is a standalone config)
if (!operation.isWorkspaceStandalone) {
const validKeys = this.configurationService.keys().default;
if (validKeys.indexOf(operation.key) < 0 && !OVERRIDE_PROPERTY_PATTERN.test(operation.key)) {
return this.wrapError(ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY, target, operation);
}
}
// Target cannot be user if is standalone
if (operation.isWorkspaceStandalone && target === ConfigurationTarget.USER) {
return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, target, operation);
}
// Target cannot be workspace or folder if no workspace opened
if ((target === ConfigurationTarget.WORKSPACE || target === ConfigurationTarget.FOLDER) && !this.contextService.hasWorkspace()) {
return this.wrapError(ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED, target, operation);
}
if (target === ConfigurationTarget.FOLDER) {
if (!operation.resource) {
return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation);
}
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
if (configurationProperties[operation.key].scope !== ConfigurationScope.RESOURCE) {
return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, target, operation);
}
}
return this.resolveModelReference(operation.resource)
.then(reference => {
const model = reference.object.textEditorModel;
if (this.hasParseErrors(model, operation)) {
return this.wrapError<typeof reference>(ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION, target, operation);
}
// Target cannot be dirty if not writing into buffer
if (checkDirty && this.textFileService.isDirty(operation.resource)) {
return this.wrapError<typeof reference>(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, target, operation);
}
return reference;
});
}
private getConfigurationEditOperation(target: ConfigurationTarget, config: IConfigurationValue, overrides: IConfigurationOverrides): IConfigurationEditOperation {
const workspace = this.contextService.getWorkspace();
// Check for standalone workspace configurations
if (config.key) {
const standaloneConfigurationKeys = Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS);
for (let i = 0; i < standaloneConfigurationKeys.length; i++) {
const key = standaloneConfigurationKeys[i];
const resource = this.getConfigurationFileResource(target, WORKSPACE_STANDALONE_CONFIGURATIONS[key], overrides.resource);
// Check for prefix
if (config.key === key) {
const jsonPath = workspace && workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath ? [key] : [];
return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource, isWorkspaceStandalone: true };
}
// Check for prefix.<setting>
const keyPrefix = `${key}.`;
if (config.key.indexOf(keyPrefix) === 0) {
const jsonPath = workspace && workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath ? [key, config.key.substr(keyPrefix.length)] : [config.key.substr(keyPrefix.length)];
return { key: jsonPath[jsonPath.length - 1], jsonPath, value: config.value, resource, isWorkspaceStandalone: true };
}
}
}
let key = config.key;
let jsonPath = overrides.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier), key] : [key];
if (target === ConfigurationTarget.USER) {
return { key, jsonPath, value: config.value, resource: URI.file(this.environmentService.appSettingsPath) };
}
const resource = this.getConfigurationFileResource(target, WORKSPACE_CONFIG_DEFAULT_PATH, overrides.resource);
if (workspace && workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath) {
jsonPath = ['settings', ...jsonPath];
}
return { key, jsonPath, value: config.value, resource };
}
private getConfigurationFileResource(target: ConfigurationTarget, relativePath: string, resource: URI): URI {
if (target === ConfigurationTarget.USER) {
return URI.file(this.environmentService.appSettingsPath);
}
const workspace = this.contextService.getWorkspace();
if (workspace) {
if (target === ConfigurationTarget.WORKSPACE) {
return this.contextService.hasMultiFolderWorkspace() ? workspace.configuration : this.toResource(relativePath, workspace.roots[0]);
}
if (target === ConfigurationTarget.FOLDER && this.contextService.hasMultiFolderWorkspace()) {
if (resource) {
const root = this.contextService.getRoot(resource);
if (root) {
return this.toResource(relativePath, root);
}
}
}
}
return null;
}
private toResource(relativePath: string, root: URI): URI {
return URI.file(paths.join(root.fsPath, relativePath));
}
}

View File

@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import * as json from 'vs/base/common/json';
import * as encoding from 'vs/base/node/encoding';
import * as strings from 'vs/base/common/strings';
import { setProperty } from 'vs/base/common/jsonEdit';
import { Queue } from 'vs/base/common/async';
import { Edit } from 'vs/base/common/jsonFormatter';
import { IReference } from 'vs/base/common/lifecycle';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService } from 'vs/platform/files/common/files';
import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IJSONEditingService, IJSONValue, JSONEditingError, JSONEditingErrorCode } from 'vs/workbench/services/configuration/common/jsonEditing';
export class JSONEditingService implements IJSONEditingService {
public _serviceBrand: any;
private queue: Queue<void>;
constructor(
@IFileService private fileService: IFileService,
@ITextModelService private textModelResolverService: ITextModelService,
@ITextFileService private textFileService: ITextFileService
) {
this.queue = new Queue<void>();
}
write(resource: URI, value: IJSONValue, save: boolean): TPromise<void> {
return this.queue.queue(() => this.doWriteConfiguration(resource, value, save)); // queue up writes to prevent race conditions
}
private doWriteConfiguration(resource: URI, value: IJSONValue, save: boolean): TPromise<void> {
return this.resolveAndValidate(resource, save)
.then(reference => this.writeToBuffer(reference.object.textEditorModel, value));
}
private writeToBuffer(model: editorCommon.IModel, value: IJSONValue): TPromise<any> {
const edit = this.getEdits(model, value)[0];
if (this.applyEditsToBuffer(edit, model)) {
return this.textFileService.save(model.uri);
}
return TPromise.as(null);
}
private applyEditsToBuffer(edit: Edit, model: editorCommon.IModel): boolean {
const startPosition = model.getPositionAt(edit.offset);
const endPosition = model.getPositionAt(edit.offset + edit.length);
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
let currentText = model.getValueInRange(range);
if (edit.content !== currentText) {
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
return true;
}
return false;
}
private getEdits(model: editorCommon.IModel, configurationValue: IJSONValue): Edit[] {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const { key, value } = configurationValue;
// Without key, the entire settings file is being replaced, so we just use JSON.stringify
if (!key) {
const content = JSON.stringify(value, null, insertSpaces ? strings.repeat(' ', tabSize) : '\t');
return [{
content,
length: content.length,
offset: 0
}];
}
return setProperty(model.getValue(), [key], value, { tabSize, insertSpaces, eol });
}
private resolveModelReference(resource: URI): TPromise<IReference<ITextEditorModel>> {
return this.fileService.existsFile(resource)
.then(exists => {
const result = exists ? TPromise.as(null) : this.fileService.updateContent(resource, '{}', { encoding: encoding.UTF8 });
return result.then(() => this.textModelResolverService.createModelReference(resource));
});
}
private hasParseErrors(model: editorCommon.IModel): boolean {
const parseErrors: json.ParseError[] = [];
json.parse(model.getValue(), parseErrors, { allowTrailingComma: true });
return parseErrors.length > 0;
}
private resolveAndValidate(resource: URI, checkDirty: boolean): TPromise<IReference<ITextEditorModel>> {
return this.resolveModelReference(resource)
.then(reference => {
const model = reference.object.textEditorModel;
if (this.hasParseErrors(model)) {
return this.wrapError<IReference<ITextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);
}
// Target cannot be dirty if not writing into buffer
if (checkDirty && this.textFileService.isDirty(resource)) {
return this.wrapError<IReference<ITextEditorModel>>(JSONEditingErrorCode.ERROR_FILE_DIRTY);
}
return reference;
});
}
private wrapError<T>(code: JSONEditingErrorCode): TPromise<T> {
const message = this.toErrorMessage(code);
return TPromise.wrapError<T>(new JSONEditingError(message, code));
}
private toErrorMessage(error: JSONEditingErrorCode): string {
switch (error) {
// User issues
case JSONEditingErrorCode.ERROR_INVALID_FILE: {
return nls.localize('errorInvalidFile', "Unable to write into the file. Please open the file to correct errors/warnings in the file and try again.");
};
case JSONEditingErrorCode.ERROR_FILE_DIRTY: {
return nls.localize('errorFileDirty', "Unable to write into the file because the file is dirty. Please save the file and try again.");
};
}
}
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { FolderConfigurationModel, ScopedConfigurationModel, FolderSettingsModel } from 'vs/workbench/services/configuration/common/configurationModels';
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
suite('ConfigurationService - Model', () => {
test('Test scoped configs are undefined', () => {
const settingsConfig = new FolderSettingsModel(JSON.stringify({
awesome: true
}));
const testObject = new FolderConfigurationModel(settingsConfig, [], ConfigurationScope.WINDOW);
assert.equal(testObject.getContentsFor('task'), undefined);
});
test('Test consolidate (settings and tasks)', () => {
const settingsConfig = new FolderSettingsModel(JSON.stringify({
awesome: true
}));
const tasksConfig = new ScopedConfigurationModel(JSON.stringify({
awesome: false
}), '', 'tasks');
const expected = {
awesome: true,
tasks: {
awesome: false
}
};
assert.deepEqual(new FolderConfigurationModel(settingsConfig, [tasksConfig], ConfigurationScope.WINDOW).contents, expected);
});
test('Test consolidate (settings and launch)', () => {
const settingsConfig = new FolderSettingsModel(JSON.stringify({
awesome: true
}));
const launchConfig = new ScopedConfigurationModel(JSON.stringify({
awesome: false
}), '', 'launch');
const expected = {
awesome: true,
launch: {
awesome: false
}
};
assert.deepEqual(new FolderConfigurationModel(settingsConfig, [launchConfig], ConfigurationScope.WINDOW).contents, expected);
});
test('Test consolidate (settings and launch and tasks) - launch/tasks wins over settings file', () => {
const settingsConfig = new FolderSettingsModel(JSON.stringify({
awesome: true,
launch: {
launchConfig: 'defined',
otherLaunchConfig: 'alsoDefined'
},
tasks: {
taskConfig: 'defined',
otherTaskConfig: 'alsoDefined'
}
}));
const tasksConfig = new ScopedConfigurationModel(JSON.stringify({
taskConfig: 'overwritten',
}), '', 'tasks');
const launchConfig = new ScopedConfigurationModel(JSON.stringify({
launchConfig: 'overwritten',
}), '', 'launch');
const expected = {
awesome: true,
launch: {
launchConfig: 'overwritten',
otherLaunchConfig: 'alsoDefined'
},
tasks: {
taskConfig: 'overwritten',
otherTaskConfig: 'alsoDefined'
}
};
assert.deepEqual(new FolderConfigurationModel(settingsConfig, [launchConfig, tasksConfig], ConfigurationScope.WINDOW).contents, expected);
assert.deepEqual(new FolderConfigurationModel(settingsConfig, [tasksConfig, launchConfig], ConfigurationScope.WINDOW).contents, expected);
});
});

View File

@@ -0,0 +1,464 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import assert = require('assert');
import os = require('os');
import path = require('path');
import fs = require('fs');
import * as sinon from 'sinon';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { Registry } from 'vs/platform/registry/common/platform';
import { ParsedArgs } from 'vs/platform/environment/common/environment';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { parseArgs } from 'vs/platform/environment/node/argv';
import extfs = require('vs/base/node/extfs');
import uuid = require('vs/base/common/uuid');
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { WorkspaceServiceImpl, WorkspaceService } from 'vs/workbench/services/configuration/node/configuration';
import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
class SettingsTestEnvironmentService extends EnvironmentService {
constructor(args: ParsedArgs, _execPath: string, private customAppSettingsHome) {
super(args, _execPath);
}
get appSettingsPath(): string { return this.customAppSettingsHome; }
}
suite('WorkspaceConfigurationService - Node', () => {
function createWorkspace(callback: (workspaceDir: string, globalSettingsFile: string, cleanUp: (callback: () => void) => void) => void): void {
const id = uuid.generateUuid();
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
const workspaceDir = path.join(parentDir, 'workspaceconfig', id);
// {{SQL CARBON EDIT}}
const workspaceSettingsDir = path.join(workspaceDir, '.sqlops');
const globalSettingsFile = path.join(workspaceDir, 'config.json');
extfs.mkdirp(workspaceSettingsDir, 493, (error) => {
callback(workspaceDir, globalSettingsFile, (callback) => extfs.del(parentDir, os.tmpdir(), () => { }, callback));
});
}
function createService(workspaceDir: string, globalSettingsFile: string): TPromise<WorkspaceService> {
const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile);
const service = new WorkspaceServiceImpl(workspaceDir, environmentService, null);
return service.initialize().then(() => service);
}
test('defaults', (done: () => void) => {
interface ITestSetting {
workspace: {
service: {
testSetting: string;
}
};
}
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test_workspace',
'type': 'object',
'properties': {
'workspace.service.testSetting': {
'type': 'string',
'default': 'isSet'
}
}
});
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
const config = service.getConfiguration<ITestSetting>();
assert.equal(config.workspace.service.testSetting, 'isSet');
service.dispose();
cleanUp(done);
});
});
});
test('globals', (done: () => void) => {
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
fs.writeFileSync(globalSettingsFile, '{ "testworkbench.editor.tabs": true }');
service.reloadConfiguration().then(() => {
const config = service.getConfiguration<{ testworkbench: { editor: { tabs: boolean } } }>();
assert.equal(config.testworkbench.editor.tabs, true);
service.dispose();
cleanUp(done);
});
});
});
});
test('reload configuration emits events', (done: () => void) => {
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
fs.writeFileSync(globalSettingsFile, '{ "testworkbench.editor.tabs": true }');
return service.initialize().then(() => {
service.onDidUpdateConfiguration(event => {
const config = service.getConfiguration<{ testworkbench: { editor: { tabs: boolean } } }>();
assert.equal(config.testworkbench.editor.tabs, false);
service.dispose();
cleanUp(done);
});
fs.writeFileSync(globalSettingsFile, '{ "testworkbench.editor.tabs": false }');
// this has to trigger the event since the config changes
service.reloadConfiguration().done();
});
});
});
});
test('globals override defaults', (done: () => void) => {
interface ITestSetting {
workspace: {
service: {
testSetting: string;
}
};
}
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test_workspace',
'type': 'object',
'properties': {
'workspace.service.testSetting': {
'type': 'string',
'default': 'isSet'
}
}
});
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
fs.writeFileSync(globalSettingsFile, '{ "workspace.service.testSetting": "isChanged" }');
service.reloadConfiguration().then(() => {
const config = service.getConfiguration<ITestSetting>();
assert.equal(config.workspace.service.testSetting, 'isChanged');
service.dispose();
cleanUp(done);
});
});
});
});
test('workspace settings', (done: () => void) => {
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
// {{SQL CARBON EDIT}}
fs.writeFileSync(path.join(workspaceDir, '.sqlops', 'settings.json'), '{ "testworkbench.editor.icons": true }');
service.reloadConfiguration().then(() => {
const config = service.getConfiguration<{ testworkbench: { editor: { icons: boolean } } }>();
assert.equal(config.testworkbench.editor.icons, true);
service.dispose();
cleanUp(done);
});
});
});
});
test('workspace settings override user settings', (done: () => void) => {
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
fs.writeFileSync(globalSettingsFile, '{ "testworkbench.editor.icons": false, "testworkbench.other.setting": true }');
// {{SQL CARBON EDIT}}
fs.writeFileSync(path.join(workspaceDir, '.sqlops', 'settings.json'), '{ "testworkbench.editor.icons": true }');
service.reloadConfiguration().then(() => {
const config = service.getConfiguration<{ testworkbench: { editor: { icons: boolean }, other: { setting: string } } }>();
assert.equal(config.testworkbench.editor.icons, true);
assert.equal(config.testworkbench.other.setting, true);
service.dispose();
cleanUp(done);
});
});
});
});
test('workspace change triggers event', (done: () => void) => {
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
service.onDidUpdateConfiguration(event => {
const config = service.getConfiguration<{ testworkbench: { editor: { icons: boolean } } }>();
assert.equal(config.testworkbench.editor.icons, true);
assert.equal(service.getConfiguration<any>().testworkbench.editor.icons, true);
service.dispose();
cleanUp(done);
});
// {{SQL CARBON EDIT}}
const settingsFile = path.join(workspaceDir, '.sqlops', 'settings.json');
fs.writeFileSync(settingsFile, '{ "testworkbench.editor.icons": true }');
const event = new FileChangesEvent([{ resource: URI.file(settingsFile), type: FileChangeType.ADDED }]);
service.handleWorkspaceFileEvents(event);
});
});
});
test('workspace reload should triggers event if content changed', (done: () => void) => {
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
// {{SQL CARBON EDIT}}
const settingsFile = path.join(workspaceDir, '.sqlops', 'settings.json');
fs.writeFileSync(settingsFile, '{ "testworkbench.editor.icons": true }');
const target = sinon.stub();
service.onDidUpdateConfiguration(event => target());
fs.writeFileSync(settingsFile, '{ "testworkbench.editor.icons": false }');
service.reloadConfiguration().done(() => {
assert.ok(target.calledOnce);
service.dispose();
cleanUp(done);
});
});
});
});
test('workspace reload should not trigger event if nothing changed', (done: () => void) => {
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
// {{SQL CARBON EDIT}}
const settingsFile = path.join(workspaceDir, '.sqlops', 'settings.json');
fs.writeFileSync(settingsFile, '{ "testworkbench.editor.icons": true }');
service.reloadConfiguration().done(() => {
const target = sinon.stub();
service.onDidUpdateConfiguration(event => target());
service.reloadConfiguration().done(() => {
assert.ok(!target.called);
service.dispose();
cleanUp(done);
});
});
});
});
});
test('workspace reload should not trigger event if there is no model', (done: () => void) => {
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
const target = sinon.stub();
service.onDidUpdateConfiguration(event => target());
service.reloadConfiguration().done(() => {
assert.ok(!target.called);
service.dispose();
cleanUp(done);
});
});
});
});
test('lookup', (done: () => void) => {
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test',
'type': 'object',
'properties': {
'workspaceLookup.service.testSetting': {
'type': 'string',
'default': 'isSet'
}
}
});
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
let res = service.lookup('something.missing');
assert.ok(!res.default);
assert.ok(!res.user);
assert.ok(!res.workspace);
assert.ok(!res.value);
res = service.lookup('workspaceLookup.service.testSetting');
assert.equal(res.default, 'isSet');
assert.equal(res.value, 'isSet');
assert.ok(!res.user);
assert.ok(!res.workspace);
fs.writeFileSync(globalSettingsFile, '{ "workspaceLookup.service.testSetting": true }');
return service.reloadConfiguration().then(() => {
res = service.lookup('workspaceLookup.service.testSetting');
assert.equal(res.default, 'isSet');
assert.equal(res.user, true);
assert.equal(res.value, true);
assert.ok(!res.workspace);
// {{SQL CARBON EDIT}}
const settingsFile = path.join(workspaceDir, '.sqlops', 'settings.json');
fs.writeFileSync(settingsFile, '{ "workspaceLookup.service.testSetting": 55 }');
return service.reloadConfiguration().then(() => {
res = service.lookup('workspaceLookup.service.testSetting');
assert.equal(res.default, 'isSet');
assert.equal(res.user, true);
assert.equal(res.workspace, 55);
assert.equal(res.value, 55);
service.dispose();
cleanUp(done);
});
});
});
});
});
test('keys', (done: () => void) => {
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test',
'type': 'object',
'properties': {
'workspaceLookup.service.testSetting': {
'type': 'string',
'default': 'isSet'
}
}
});
function contains(array: string[], key: string): boolean {
return array.indexOf(key) >= 0;
}
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
let keys = service.keys();
assert.ok(!contains(keys.default, 'something.missing'));
assert.ok(!contains(keys.user, 'something.missing'));
assert.ok(!contains(keys.workspace, 'something.missing'));
assert.ok(contains(keys.default, 'workspaceLookup.service.testSetting'));
assert.ok(!contains(keys.user, 'workspaceLookup.service.testSetting'));
assert.ok(!contains(keys.workspace, 'workspaceLookup.service.testSetting'));
fs.writeFileSync(globalSettingsFile, '{ "workspaceLookup.service.testSetting": true }');
return service.reloadConfiguration().then(() => {
keys = service.keys();
assert.ok(contains(keys.default, 'workspaceLookup.service.testSetting'));
assert.ok(contains(keys.user, 'workspaceLookup.service.testSetting'));
assert.ok(!contains(keys.workspace, 'workspaceLookup.service.testSetting'));
// {{SQL CARBON EDIT}}
const settingsFile = path.join(workspaceDir, '.sqlops', 'settings.json');
fs.writeFileSync(settingsFile, '{ "workspaceLookup.service.testSetting": 55 }');
return service.reloadConfiguration().then(() => {
keys = service.keys();
assert.ok(contains(keys.default, 'workspaceLookup.service.testSetting'));
assert.ok(contains(keys.user, 'workspaceLookup.service.testSetting'));
assert.ok(contains(keys.workspace, 'workspaceLookup.service.testSetting'));
// {{SQL CARBON EDIT}}
const settingsFile = path.join(workspaceDir, '.sqlops', 'tasks.json');
fs.writeFileSync(settingsFile, '{ "workspaceLookup.service.taskTestSetting": 55 }');
return service.reloadConfiguration().then(() => {
keys = service.keys();
assert.ok(!contains(keys.default, 'tasks.workspaceLookup.service.taskTestSetting'));
assert.ok(!contains(keys.user, 'tasks.workspaceLookup.service.taskTestSetting'));
assert.ok(contains(keys.workspace, 'tasks.workspaceLookup.service.taskTestSetting'));
service.dispose();
cleanUp(done);
});
});
});
});
});
});
test('values', (done: () => void) => {
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test',
'type': 'object',
'properties': {
'workspaceLookup.service.testSetting': {
'type': 'string',
'default': 'isSet'
}
}
});
createWorkspace((workspaceDir, globalSettingsFile, cleanUp) => {
return createService(workspaceDir, globalSettingsFile).then(service => {
let values = service.values();
let value = values['workspaceLookup.service.testSetting'];
assert.ok(value);
assert.equal(value.default, 'isSet');
fs.writeFileSync(globalSettingsFile, '{ "workspaceLookup.service.testSetting": true }');
return service.reloadConfiguration().then(() => {
values = service.values();
value = values['workspaceLookup.service.testSetting'];
assert.ok(value);
assert.equal(value.user, true);
// {{SQL CARBON EDIT}}
const settingsFile = path.join(workspaceDir, '.sqlops', 'settings.json');
fs.writeFileSync(settingsFile, '{ "workspaceLookup.service.testSetting": 55 }');
return service.reloadConfiguration().then(() => {
values = service.values();
value = values['workspaceLookup.service.testSetting'];
assert.ok(value);
assert.equal(value.user, true);
assert.equal(value.workspace, 55);
done();
});
});
});
});
});
});

View File

@@ -0,0 +1,315 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as sinon from 'sinon';
import assert = require('assert');
import os = require('os');
import path = require('path');
import fs = require('fs');
import * as json from 'vs/base/common/json';
import { TPromise } from 'vs/base/common/winjs.base';
import { Registry } from 'vs/platform/registry/common/platform';
import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/environment';
import { parseArgs } from 'vs/platform/environment/node/argv';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import extfs = require('vs/base/node/extfs');
import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService } from 'vs/workbench/test/workbenchTestServices';
import uuid = require('vs/base/common/uuid');
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { WorkspaceService, EmptyWorkspaceServiceImpl, WorkspaceServiceImpl } from 'vs/workbench/services/configuration/node/configuration';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { ConfigurationTarget, ConfigurationEditingError, ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/common/configurationEditing';
import { IFileService } from 'vs/platform/files/common/files';
import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { IChoiceService, IMessageService } from 'vs/platform/message/common/message';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
class SettingsTestEnvironmentService extends EnvironmentService {
constructor(args: ParsedArgs, _execPath: string, private customAppSettingsHome) {
super(args, _execPath);
}
get appSettingsPath(): string { return this.customAppSettingsHome; }
}
suite('ConfigurationEditingService', () => {
let instantiationService: TestInstantiationService;
let testObject: ConfigurationEditingService;
let parentDir;
let workspaceDir;
let globalSettingsFile;
let workspaceSettingsDir;
let choiceService;
suiteSetup(() => {
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test',
'type': 'object',
'properties': {
'configurationEditing.service.testSetting': {
'type': 'string',
'default': 'isSet'
},
'configurationEditing.service.testSettingTwo': {
'type': 'string',
'default': 'isSet'
},
'configurationEditing.service.testSettingThree': {
'type': 'string',
'default': 'isSet'
}
}
});
});
setup(() => {
return setUpWorkspace()
.then(() => setUpServices());
});
function setUpWorkspace(): TPromise<void> {
return new TPromise<void>((c, e) => {
const id = uuid.generateUuid();
parentDir = path.join(os.tmpdir(), 'vsctests', id);
workspaceDir = path.join(parentDir, 'workspaceconfig', id);
globalSettingsFile = path.join(workspaceDir, 'config.json');
// {{SQL CARBON EDIT}}
workspaceSettingsDir = path.join(workspaceDir, '.sqlops');
extfs.mkdirp(workspaceSettingsDir, 493, (error) => {
if (error) {
e(error);
} else {
c(null);
}
});
});
}
function setUpServices(noWorkspace: boolean = false): TPromise<void> {
// Clear services if they are already created
clearServices();
instantiationService = new TestInstantiationService();
const environmentService = new SettingsTestEnvironmentService(parseArgs(process.argv), process.execPath, globalSettingsFile);
instantiationService.stub(IEnvironmentService, environmentService);
const workspacesService = instantiationService.stub(IWorkspacesService, {});
const workspaceService = noWorkspace ? new EmptyWorkspaceServiceImpl(environmentService) : new WorkspaceServiceImpl(workspaceDir, environmentService, workspacesService);
instantiationService.stub(IWorkspaceContextService, workspaceService);
instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(ILifecycleService, new TestLifecycleService());
instantiationService.stub(IEditorGroupService, new TestEditorGroupService());
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModeService, ModeServiceImpl);
instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl));
instantiationService.stub(IFileService, new FileService(workspaceService, new TestConfigurationService(), { disableWatcher: true }));
instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
instantiationService.stub(IBackupFileService, new TestBackupFileService());
choiceService = instantiationService.stub(IChoiceService, {
choose: (severity, message, options, cancelId): TPromise<number> => {
return TPromise.as(cancelId);
}
});
instantiationService.stub(IMessageService, {
show: (severity, message, options, cancelId): void => { }
});
testObject = instantiationService.createInstance(ConfigurationEditingService);
return workspaceService.initialize();
}
teardown(() => {
clearServices();
return clearWorkspace();
});
function clearServices(): void {
if (instantiationService) {
const configuraitonService = <WorkspaceService>instantiationService.get(IConfigurationService);
if (configuraitonService) {
configuraitonService.dispose();
}
instantiationService = null;
}
}
function clearWorkspace(): TPromise<void> {
return new TPromise<void>((c, e) => {
if (parentDir) {
extfs.del(parentDir, os.tmpdir(), () => c(null), () => c(null));
} else {
c(null);
}
}).then(() => parentDir = null);
}
test('errors cases - invalid key', () => {
return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'unknown.key', value: 'value' })
.then(() => assert.fail('Should fail with ERROR_UNKNOWN_KEY'),
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY));
});
test('errors cases - invalid target', () => {
return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'tasks.something', value: 'value' })
.then(() => assert.fail('Should fail with ERROR_INVALID_TARGET'),
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET));
});
test('errors cases - no workspace', () => {
return setUpServices(true)
.then(() => testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'configurationEditing.service.testSetting', value: 'value' }))
.then(() => assert.fail('Should fail with ERROR_NO_WORKSPACE_OPENED'),
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED));
});
test('errors cases - invalid configuration', () => {
fs.writeFileSync(globalSettingsFile, ',,,,,,,,,,,,,,');
return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' })
.then(() => assert.fail('Should fail with ERROR_INVALID_CONFIGURATION'),
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION));
});
test('errors cases - dirty', () => {
instantiationService.stub(ITextFileService, 'isDirty', true);
return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' })
.then(() => assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'),
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY));
});
test('dirty error is not thrown if not asked to save', () => {
instantiationService.stub(ITextFileService, 'isDirty', true);
return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotSave: true })
.then(() => null, error => assert.fail('Should not fail.'));
});
test('do not notify error', () => {
instantiationService.stub(ITextFileService, 'isDirty', true);
const target = sinon.stub();
instantiationService.stubPromise(IChoiceService, 'choose', target);
return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' }, { donotNotifyError: true })
.then(() => assert.fail('Should fail with ERROR_CONFIGURATION_FILE_DIRTY error.'),
(error: ConfigurationEditingError) => {
assert.equal(false, target.calledOnce);
assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY);
});
});
test('write one setting - empty file', () => {
return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' })
.then(() => {
const contents = fs.readFileSync(globalSettingsFile).toString('utf8');
const parsed = json.parse(contents);
assert.equal(parsed['configurationEditing.service.testSetting'], 'value');
assert.equal(instantiationService.get(IConfigurationService).lookup('configurationEditing.service.testSetting').value, 'value');
});
});
test('write one setting - existing file', () => {
fs.writeFileSync(globalSettingsFile, '{ "my.super.setting": "my.super.value" }');
return testObject.writeConfiguration(ConfigurationTarget.USER, { key: 'configurationEditing.service.testSetting', value: 'value' })
.then(() => {
const contents = fs.readFileSync(globalSettingsFile).toString('utf8');
const parsed = json.parse(contents);
assert.equal(parsed['configurationEditing.service.testSetting'], 'value');
assert.equal(parsed['my.super.setting'], 'my.super.value');
const configurationService = instantiationService.get(IConfigurationService);
assert.equal(configurationService.lookup('configurationEditing.service.testSetting').value, 'value');
assert.equal(configurationService.lookup('my.super.setting').value, 'my.super.value');
});
});
test('write workspace standalone setting - empty file', () => {
return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks.service.testSetting', value: 'value' })
.then(() => {
const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']);
const contents = fs.readFileSync(target).toString('utf8');
const parsed = json.parse(contents);
assert.equal(parsed['service.testSetting'], 'value');
const configurationService = instantiationService.get(IConfigurationService);
assert.equal(configurationService.lookup('tasks.service.testSetting').value, 'value');
});
});
test('write workspace standalone setting - existing file', () => {
const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['launch']);
fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }');
return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'launch.service.testSetting', value: 'value' })
.then(() => {
const contents = fs.readFileSync(target).toString('utf8');
const parsed = json.parse(contents);
assert.equal(parsed['service.testSetting'], 'value');
assert.equal(parsed['my.super.setting'], 'my.super.value');
const configurationService = instantiationService.get(IConfigurationService);
assert.equal(configurationService.lookup('launch.service.testSetting').value, 'value');
assert.equal(configurationService.lookup('launch.my.super.setting').value, 'my.super.value');
});
});
test('write workspace standalone setting - empty file - full JSON', () => {
return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } })
.then(() => {
const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']);
const contents = fs.readFileSync(target).toString('utf8');
const parsed = json.parse(contents);
assert.equal(parsed['version'], '1.0.0');
assert.equal(parsed['tasks'][0]['taskName'], 'myTask');
});
});
test('write workspace standalone setting - existing file - full JSON', () => {
const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['launch']);
fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }');
return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } })
.then(() => {
const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']);
const contents = fs.readFileSync(target).toString('utf8');
const parsed = json.parse(contents);
assert.equal(parsed['version'], '1.0.0');
assert.equal(parsed['tasks'][0]['taskName'], 'myTask');
});
});
test('write workspace standalone setting - existing file with JSON errors - full JSON', () => {
const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['launch']);
fs.writeFileSync(target, '{ "my.super.setting": '); // invalid JSON
return testObject.writeConfiguration(ConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } })
.then(() => {
const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']);
const contents = fs.readFileSync(target).toString('utf8');
const parsed = json.parse(contents);
assert.equal(parsed['version'], '1.0.0');
assert.equal(parsed['tasks'][0]['taskName'], 'myTask');
});
});
});

View File

@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import uri from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { IStringDictionary } from 'vs/base/common/collections';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IConfigurationResolverService = createDecorator<IConfigurationResolverService>('configurationResolverService');
export interface IConfigurationResolverService {
_serviceBrand: any;
// TODO@Isidor improve this API
resolve(root: uri, value: string): string;
resolve(root: uri, value: string[]): string[];
resolve(root: uri, value: IStringDictionary<string>): IStringDictionary<string>;
resolveAny<T>(root: uri, value: T): T;
resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string }): TPromise<any>;
}

View File

@@ -0,0 +1,263 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import uri from 'vs/base/common/uri';
import * as paths from 'vs/base/common/paths';
import * as types from 'vs/base/common/types';
import { TPromise } from 'vs/base/common/winjs.base';
import { sequence } from 'vs/base/common/async';
import { IStringDictionary } from 'vs/base/common/collections';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { ICommonCodeEditor } from 'vs/editor/common/editorCommon';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { toResource } from 'vs/workbench/common/editor';
export class ConfigurationResolverService implements IConfigurationResolverService {
_serviceBrand: any;
private _execPath: string;
private _workspaceRoot: string;
constructor(
envVariables: { [key: string]: string },
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IEnvironmentService environmentService: IEnvironmentService,
@IConfigurationService private configurationService: IConfigurationService,
@ICommandService private commandService: ICommandService,
) {
this._execPath = environmentService.execPath;
Object.keys(envVariables).forEach(key => {
this[`env:${key}`] = envVariables[key];
});
}
private get execPath(): string {
return this._execPath;
}
private get cwd(): string {
return this.workspaceRoot;
}
private get workspaceRoot(): string {
return this._workspaceRoot;
}
private get workspaceRootFolderName(): string {
return this.workspaceRoot ? paths.basename(this.workspaceRoot) : '';
}
private get file(): string {
return this.getFilePath();
}
private get relativeFile(): string {
return (this.workspaceRoot) ? paths.relative(this.workspaceRoot, this.file) : this.file;
}
private get fileBasename(): string {
return paths.basename(this.getFilePath());
}
private get fileBasenameNoExtension(): string {
const basename = this.fileBasename;
return basename.slice(0, basename.length - paths.extname(basename).length);
}
private get fileDirname(): string {
return paths.dirname(this.getFilePath());
}
private get fileExtname(): string {
return paths.extname(this.getFilePath());
}
private get lineNumber(): string {
const activeEditor = this.editorService.getActiveEditor();
if (activeEditor) {
const editorControl = (<ICommonCodeEditor>activeEditor.getControl());
if (editorControl) {
const lineNumber = editorControl.getSelection().positionLineNumber;
return String(lineNumber);
}
}
return '';
}
private getFilePath(): string {
let input = this.editorService.getActiveEditorInput();
if (!input) {
return '';
}
let fileResource = toResource(input, { filter: 'file' });
if (!fileResource) {
return '';
}
return paths.normalize(fileResource.fsPath, true);
}
public resolve(root: uri, value: string): string;
public resolve(root: uri, value: string[]): string[];
public resolve(root: uri, value: IStringDictionary<string>): IStringDictionary<string>;
public resolve(root: uri, value: any): any {
this._workspaceRoot = root.fsPath.toString();
if (types.isString(value)) {
return this.resolveString(root, value);
} else if (types.isArray(value)) {
return this.resolveArray(root, value);
} else if (types.isObject(value)) {
return this.resolveLiteral(root, value);
}
return value;
}
public resolveAny<T>(root: uri, value: T): T;
public resolveAny<T>(root: uri, value: any): any {
this._workspaceRoot = root.fsPath.toString();
if (types.isString(value)) {
return this.resolveString(root, value);
} else if (types.isArray(value)) {
return this.resolveAnyArray(root, value);
} else if (types.isObject(value)) {
return this.resolveAnyLiteral(root, value);
}
return value;
}
private resolveString(root: uri, value: string): string {
let regexp = /\$\{(.*?)\}/g;
const originalValue = value;
const resolvedString = value.replace(regexp, (match: string, name: string) => {
let newValue = (<any>this)[name];
if (types.isString(newValue)) {
return newValue;
} else {
return match && match.indexOf('env:') > 0 ? '' : match;
}
});
return this.resolveConfigVariable(root, resolvedString, originalValue);
}
private resolveConfigVariable(root: uri, value: string, originalValue: string): string {
const replacer = (match: string, name: string) => {
let config = this.configurationService.getConfiguration<any>();
let newValue: any;
try {
const keys: string[] = name.split('.');
if (!keys || keys.length <= 0) {
return '';
}
while (keys.length > 1) {
const key = keys.shift();
if (!config || !config.hasOwnProperty(key)) {
return '';
}
config = config[key];
}
newValue = config && config.hasOwnProperty(keys[0]) ? config[keys[0]] : '';
} catch (e) {
return '';
}
if (types.isString(newValue)) {
// Prevent infinite recursion and also support nested references (or tokens)
return newValue === originalValue ? '' : this.resolveString(root, newValue);
} else {
return this.resolve(root, newValue) + '';
}
};
return value.replace(/\$\{config:(.+?)\}/g, replacer);
}
private resolveLiteral(root: uri, values: IStringDictionary<string | IStringDictionary<string> | string[]>): IStringDictionary<string | IStringDictionary<string> | string[]> {
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
Object.keys(values).forEach(key => {
let value = values[key];
result[key] = <any>this.resolve(root, <any>value);
});
return result;
}
private resolveAnyLiteral<T>(root: uri, values: T): T;
private resolveAnyLiteral<T>(root: uri, values: any): any {
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
Object.keys(values).forEach(key => {
let value = values[key];
result[key] = <any>this.resolveAny(root, <any>value);
});
return result;
}
private resolveArray(root: uri, value: string[]): string[] {
return value.map(s => this.resolveString(root, s));
}
private resolveAnyArray<T>(root: uri, value: T[]): T[];
private resolveAnyArray(root: uri, value: any[]): any[] {
return value.map(s => this.resolveAny(root, s));
}
/**
* Resolve all interactive variables in configuration #6569
*/
public resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string }): TPromise<any> {
if (!configuration) {
return TPromise.as(null);
}
// We need a map from interactive variables to keys because we only want to trigger an command once per key -
// even though it might occur multiple times in configuration #7026.
const interactiveVariablesToSubstitutes: { [interactiveVariable: string]: { object: any, key: string }[] } = {};
const findInteractiveVariables = (object: any) => {
Object.keys(object).forEach(key => {
if (object[key] && typeof object[key] === 'object') {
findInteractiveVariables(object[key]);
} else if (typeof object[key] === 'string') {
const matches = /\${command:(.+)}/.exec(object[key]);
if (matches && matches.length === 2) {
const interactiveVariable = matches[1];
if (!interactiveVariablesToSubstitutes[interactiveVariable]) {
interactiveVariablesToSubstitutes[interactiveVariable] = [];
}
interactiveVariablesToSubstitutes[interactiveVariable].push({ object, key });
}
}
});
};
findInteractiveVariables(configuration);
let substitionCanceled = false;
const factory: { (): TPromise<any> }[] = Object.keys(interactiveVariablesToSubstitutes).map(interactiveVariable => {
return () => {
let commandId: string = null;
commandId = interactiveVariablesMap ? interactiveVariablesMap[interactiveVariable] : null;
if (!commandId) {
// Just launch any command if the interactive variable is not contributed by the adapter #12735
commandId = interactiveVariable;
}
return this.commandService.executeCommand<string>(commandId, configuration).then(result => {
if (result) {
interactiveVariablesToSubstitutes[interactiveVariable].forEach(substitute => {
if (substitute.object[substitute.key].indexOf(`\${command:${interactiveVariable}}`) >= 0) {
substitute.object[substitute.key] = substitute.object[substitute.key].replace(`\${command:${interactiveVariable}}`, result);
}
});
} else {
substitionCanceled = true;
}
});
};
});
return sequence(factory).then(() => substitionCanceled ? null : configuration);
}
}

View File

@@ -0,0 +1,365 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert = require('assert');
import uri from 'vs/base/common/uri';
import platform = require('vs/base/common/platform');
import { TPromise } from 'vs/base/common/winjs.base';
import { IConfigurationService, getConfigurationValue, IConfigurationOverrides, IConfigurationValue } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/node/configurationResolverService';
import { TestEnvironmentService, TestEditorService } from 'vs/workbench/test/workbenchTestServices';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
suite('Configuration Resolver Service', () => {
let configurationResolverService: IConfigurationResolverService;
let envVariables: { [key: string]: string } = { key1: 'Value for Key1', key2: 'Value for Key2' };
let mockCommandService: MockCommandService;
let editorService: TestEditorService;
let workspaceUri: uri;
setup(() => {
mockCommandService = new MockCommandService();
editorService = new TestEditorService();
workspaceUri = uri.parse('file:///VSCode/workspaceLocation');
configurationResolverService = new ConfigurationResolverService(envVariables, editorService, TestEnvironmentService, new TestConfigurationService(), mockCommandService);
});
teardown(() => {
configurationResolverService = null;
});
test('substitute one', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, 'abc ${workspaceRoot} xyz'), 'abc \\VSCode\\workspaceLocation xyz');
} else {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, 'abc ${workspaceRoot} xyz'), 'abc /VSCode/workspaceLocation xyz');
}
});
test('workspace root folder name', () => {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, 'abc ${workspaceRootFolderName} xyz'), 'abc workspaceLocation xyz');
});
test('current selected line number', () => {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, 'abc ${lineNumber} xyz'), `abc ${editorService.mockLineNumber} xyz`);
});
test('substitute many', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, '${workspaceRoot} - ${workspaceRoot}'), '\\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation');
} else {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, '${workspaceRoot} - ${workspaceRoot}'), '/VSCode/workspaceLocation - /VSCode/workspaceLocation');
}
});
test('substitute one env variable', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, 'abc ${workspaceRoot} ${env:key1} xyz'), 'abc \\VSCode\\workspaceLocation Value for Key1 xyz');
} else {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, 'abc ${workspaceRoot} ${env:key1} xyz'), 'abc /VSCode/workspaceLocation Value for Key1 xyz');
}
});
test('substitute many env variable', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, '${workspaceRoot} - ${workspaceRoot} ${env:key1} - ${env:key2}'), '\\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for Key1 - Value for Key2');
} else {
assert.strictEqual(configurationResolverService.resolve(workspaceUri, '${workspaceRoot} - ${workspaceRoot} ${env:key1} - ${env:key2}'), '/VSCode/workspaceLocation - /VSCode/workspaceLocation Value for Key1 - Value for Key2');
}
});
test('substitute one configuration variable', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
fontFamily: 'foo'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} xyz'), 'abc foo xyz');
});
test('substitute many configuration variables', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
fontFamily: 'foo'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo bar xyz');
});
test('substitute nested configuration variables', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
fontFamily: 'foo ${workspaceRoot} ${config:terminal.integrated.fontFamily}'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
if (platform.isWindows) {
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo \\VSCode\\workspaceLocation bar bar xyz');
} else {
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo /VSCode/workspaceLocation bar bar xyz');
}
});
test('substitute accidental self referenced configuration variables', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
fontFamily: 'foo ${workspaceRoot} ${config:terminal.integrated.fontFamily} ${config:editor.fontFamily}'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
if (platform.isWindows) {
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo \\VSCode\\workspaceLocation bar bar xyz');
} else {
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} xyz'), 'abc foo /VSCode/workspaceLocation bar bar xyz');
}
});
test('substitute one env variable and a configuration variable', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
fontFamily: 'foo'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
if (platform.isWindows) {
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} ${workspaceRoot} ${env:key1} xyz'), 'abc foo \\VSCode\\workspaceLocation Value for Key1 xyz');
} else {
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} ${workspaceRoot} ${env:key1} xyz'), 'abc foo /VSCode/workspaceLocation Value for Key1 xyz');
}
});
test('substitute many env variable and a configuration variable', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
fontFamily: 'foo'
},
terminal: {
integrated: {
fontFamily: 'bar'
}
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
if (platform.isWindows) {
assert.strictEqual(service.resolve(workspaceUri, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceRoot} - ${workspaceRoot} ${env:key1} - ${env:key2}'), 'foo bar \\VSCode\\workspaceLocation - \\VSCode\\workspaceLocation Value for Key1 - Value for Key2');
} else {
assert.strictEqual(service.resolve(workspaceUri, '${config:editor.fontFamily} ${config:terminal.integrated.fontFamily} ${workspaceRoot} - ${workspaceRoot} ${env:key1} - ${env:key2}'), 'foo bar /VSCode/workspaceLocation - /VSCode/workspaceLocation Value for Key1 - Value for Key2');
}
});
test('mixed types of configuration variables', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
fontFamily: 'foo',
lineNumbers: 123,
insertSpaces: false
},
terminal: {
integrated: {
fontFamily: 'bar'
}
},
json: {
schemas: [
{
fileMatch: [
'/myfile',
'/myOtherfile'
],
url: 'schemaURL'
}
]
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.fontFamily} ${config:editor.lineNumbers} ${config:editor.insertSpaces} xyz'), 'abc foo 123 false xyz');
});
test('configuration should not evaluate Javascript', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
abc: 'foo'
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor[\'abc\'.substr(0)]} xyz'), 'abc xyz');
});
test('uses empty string as fallback', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.abc} xyz'), 'abc xyz');
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.abc.def} xyz'), 'abc xyz');
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:panel} xyz'), 'abc xyz');
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:panel.abc} xyz'), 'abc xyz');
});
test('is restricted to own properties', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.__proto__} xyz'), 'abc xyz');
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.toString} xyz'), 'abc xyz');
});
test('configuration variables with invalid accessor', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
editor: {
fontFamily: 'foo'
}
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService);
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:} xyz'), 'abc ${config:} xyz');
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor..fontFamily} xyz'), 'abc xyz');
assert.strictEqual(service.resolve(workspaceUri, 'abc ${config:editor.none.none2} xyz'), 'abc xyz');
});
test('interactive variable simple', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:interactiveVariable1}',
'port': 5858,
'sourceMaps': false,
'outDir': null
};
const interactiveVariables = Object.create(null);
interactiveVariables['interactiveVariable1'] = 'command1';
interactiveVariables['interactiveVariable2'] = 'command2';
configurationResolverService.resolveInteractiveVariables(configuration, interactiveVariables).then(resolved => {
assert.deepEqual(resolved, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1',
'port': 5858,
'sourceMaps': false,
'outDir': null
});
assert.equal(1, mockCommandService.callCount);
});
});
test('interactive variable complex', () => {
const configuration = {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': '${command:interactiveVariable1}',
'port': '${command:interactiveVariable2}',
'sourceMaps': false,
'outDir': 'src/${command:interactiveVariable2}',
'env': {
'processId': '__${command:interactiveVariable2}__',
}
};
const interactiveVariables = Object.create(null);
interactiveVariables['interactiveVariable1'] = 'command1';
interactiveVariables['interactiveVariable2'] = 'command2';
configurationResolverService.resolveInteractiveVariables(configuration, interactiveVariables).then(resolved => {
assert.deepEqual(resolved, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
'processId': 'command1',
'port': 'command2',
'sourceMaps': false,
'outDir': 'src/command2',
'env': {
'processId': '__command2__',
}
});
assert.equal(2, mockCommandService.callCount);
});
});
});
class MockConfigurationService implements IConfigurationService {
public _serviceBrand: any;
public serviceId = IConfigurationService;
public constructor(private configuration: any = {}) { }
public reloadConfiguration<T>(section?: string): TPromise<T> { return TPromise.as(this.getConfiguration()); }
public lookup<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> { return { value: getConfigurationValue<T>(this.getConfiguration(), key), default: getConfigurationValue<T>(this.getConfiguration(), key), user: getConfigurationValue<T>(this.getConfiguration(), key), workspace: void 0, folder: void 0 }; }
public keys() { return { default: [], user: [], workspace: [], folder: [] }; }
public values() { return {}; }
public getConfiguration(): any { return this.configuration; }
public getConfigurationData(): any { return null; }
public onDidUpdateConfiguration() { return { dispose() { } }; }
}
class MockCommandService implements ICommandService {
public _serviceBrand: any;
public callCount = 0;
onWillExecuteCommand = () => ({ dispose: () => { } });
public executeCommand<T>(commandId: string, ...args: any[]): TPromise<any> {
this.callCount++;
return TPromise.as(commandId);
}
}

View File

@@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import severity from 'vs/base/common/severity';
import { IAction, IActionRunner, ActionRunner } from 'vs/base/common/actions';
import { Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import dom = require('vs/base/browser/dom');
import { IContextMenuService, IContextMenuDelegate, ContextSubMenu, IEvent } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IMessageService } from 'vs/platform/message/common/message';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { remote, webFrame } from 'electron';
import { unmnemonicLabel } from 'vs/base/common/labels';
export class ContextMenuService implements IContextMenuService {
public _serviceBrand: any;
constructor(
@IMessageService private messageService: IMessageService,
@ITelemetryService private telemetryService: ITelemetryService,
@IKeybindingService private keybindingService: IKeybindingService
) {
}
public showContextMenu(delegate: IContextMenuDelegate): void {
delegate.getActions().then(actions => {
if (!actions.length) {
return TPromise.as(null);
}
return TPromise.timeout(0).then(() => { // https://github.com/Microsoft/vscode/issues/3638
const menu = this.createMenu(delegate, actions);
const anchor = delegate.getAnchor();
let x: number, y: number;
if (dom.isHTMLElement(anchor)) {
let elementPosition = dom.getDomNodePagePosition(anchor);
x = elementPosition.left;
y = elementPosition.top + elementPosition.height;
} else {
const pos = <{ x: number; y: number; }>anchor;
x = pos.x;
y = pos.y;
}
let zoom = webFrame.getZoomFactor();
x *= zoom;
y *= zoom;
menu.popup(remote.getCurrentWindow(), Math.floor(x), Math.floor(y));
if (delegate.onHide) {
delegate.onHide(undefined);
}
});
});
}
private createMenu(delegate: IContextMenuDelegate, entries: (IAction | ContextSubMenu)[]): Electron.Menu {
const menu = new remote.Menu();
const actionRunner = delegate.actionRunner || new ActionRunner();
entries.forEach(e => {
if (e instanceof Separator) {
menu.append(new remote.MenuItem({ type: 'separator' }));
} else if (e instanceof ContextSubMenu) {
const submenu = new remote.MenuItem({
submenu: this.createMenu(delegate, e.entries),
label: unmnemonicLabel(e.label)
});
menu.append(submenu);
} else {
const options: Electron.MenuItemOptions = {
label: unmnemonicLabel(e.label),
checked: !!e.checked || !!e.radio,
type: !!e.checked ? 'checkbox' : !!e.radio ? 'radio' : void 0,
enabled: !!e.enabled,
click: (menuItem, win, event) => {
this.runAction(actionRunner, e, delegate, event);
}
};
const keybinding = !!delegate.getKeyBinding ? delegate.getKeyBinding(e) : this.keybindingService.lookupKeybinding(e.id);
if (keybinding) {
const electronAccelerator = keybinding.getElectronAccelerator();
if (electronAccelerator) {
options.accelerator = electronAccelerator;
} else {
const label = keybinding.getLabel();
if (label) {
options.label = `${options.label} [${label}]`;
}
}
}
const item = new remote.MenuItem(options);
menu.append(item);
}
});
return menu;
}
private runAction(actionRunner: IActionRunner, actionToRun: IAction, delegate: IContextMenuDelegate, event: IEvent): void {
this.telemetryService.publicLog('workbenchActionExecuted', { id: actionToRun.id, from: 'contextMenu' });
const context = delegate.getActionsContext ? delegate.getActionsContext(event) : event;
const res = actionRunner.run(actionToRun, context) || TPromise.as(null);
res.done(null, e => this.messageService.show(severity.Error, e));
}
}

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const ICrashReporterService = createDecorator<ICrashReporterService>('crashReporterService');
export const TELEMETRY_SECTION_ID = 'telemetry';
export interface ICrashReporterConfig {
enableCrashReporter: boolean;
}
const configurationRegistry = <IConfigurationRegistry>Registry.as(Extensions.Configuration);
configurationRegistry.registerConfiguration({
'id': TELEMETRY_SECTION_ID,
'order': 110,
title: nls.localize('telemetryConfigurationTitle', "Telemetry"),
'type': 'object',
'properties': {
'telemetry.enableCrashReporter': {
'type': 'boolean',
'description': nls.localize('telemetry.enableCrashReporting', "Enable crash reports to be sent to Microsoft.\nThis option requires restart to take effect."),
'default': true
}
}
});
export interface ICrashReporterService {
_serviceBrand: any;
getChildProcessStartOptions(processName: string): Electron.CrashReporterStartOptions;
}
export const NullCrashReporterService: ICrashReporterService = {
_serviceBrand: undefined,
getChildProcessStartOptions(processName: string) { return undefined; }
};

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { onUnexpectedError } from 'vs/base/common/errors';
import { assign, clone } from 'vs/base/common/objects';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { crashReporter } from 'electron';
import product from 'vs/platform/node/product';
import pkg from 'vs/platform/node/package';
import * as os from 'os';
import { ICrashReporterService, TELEMETRY_SECTION_ID, ICrashReporterConfig } from 'vs/workbench/services/crashReporter/common/crashReporterService';
import { isWindows, isMacintosh, isLinux } from 'vs/base/common/platform';
export class CrashReporterService implements ICrashReporterService {
public _serviceBrand: any;
private options: Electron.CrashReporterStartOptions;
private isEnabled: boolean;
constructor(
@ITelemetryService private telemetryService: ITelemetryService,
@IWindowsService private windowsService: IWindowsService,
@IConfigurationService configurationService: IConfigurationService
) {
const config = configurationService.getConfiguration<ICrashReporterConfig>(TELEMETRY_SECTION_ID);
this.isEnabled = !!config.enableCrashReporter;
if (this.isEnabled) {
this.startCrashReporter();
}
}
private startCrashReporter(): void {
// base options with product info
this.options = {
companyName: product.crashReporter.companyName,
productName: product.crashReporter.productName,
submitURL: this.getSubmitURL(),
extra: {
vscode_version: pkg.version,
vscode_commit: product.commit
}
};
// mixin telemetry info
this.telemetryService.getTelemetryInfo()
.then(info => {
assign(this.options.extra, {
vscode_sessionId: info.sessionId,
vscode_machineId: info.machineId
});
// start crash reporter right here
crashReporter.start(clone(this.options));
// start crash reporter in the main process
return this.windowsService.startCrashReporter(this.options);
})
.done(null, onUnexpectedError);
}
private getSubmitURL(): string {
let submitURL: string;
if (isWindows) {
submitURL = product.hockeyApp[`win32-${process.arch}`];
} else if (isMacintosh) {
submitURL = product.hockeyApp.darwin;
} else if (isLinux) {
submitURL = product.hockeyApp[`linux-${process.arch}`];
}
return submitURL;
}
public getChildProcessStartOptions(name: string): Electron.CrashReporterStartOptions {
// Experimental crash reporting support for child processes on Mac only for now
if (this.isEnabled && isMacintosh) {
const childProcessOptions = clone(this.options);
childProcessOptions.extra.processName = name;
childProcessOptions.crashesDirectory = os.tmpdir();
return childProcessOptions;
}
return void 0;
}
}

View File

@@ -0,0 +1,363 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import network = require('vs/base/common/network');
import { Registry } from 'vs/platform/registry/common/platform';
import { basename, dirname } from 'vs/base/common/paths';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions, TextEditorOptions, IEditorRegistry, Extensions, SideBySideEditorInput, IFileEditorInput, IFileInputFactory } from 'vs/workbench/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { IUntitledEditorService, UNTITLED_SCHEMA } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IWorkbenchEditorService, IResourceInputType } from 'vs/workbench/services/editor/common/editorService';
import { IEditorInput, IEditorOptions, ITextEditorOptions, Position, Direction, IEditor, IResourceInput, IResourceDiffInput, IResourceSideBySideInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import nls = require('vs/nls');
import { getPathLabel } from 'vs/base/common/labels';
import { ResourceMap } from 'vs/base/common/map';
import { once } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
export interface IEditorPart {
openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, sideBySide?: boolean): TPromise<BaseEditor>;
openEditor(input?: IEditorInput, options?: IEditorOptions | ITextEditorOptions, position?: Position): TPromise<BaseEditor>;
openEditors(editors: { input: IEditorInput, position: Position, options?: IEditorOptions | ITextEditorOptions }[]): TPromise<BaseEditor[]>;
replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], position?: Position): TPromise<BaseEditor[]>;
closeEditor(position: Position, input: IEditorInput): TPromise<void>;
closeEditors(position: Position, filter?: { except?: IEditorInput, direction?: Direction, unmodifiedOnly?: boolean }): TPromise<void>;
closeAllEditors(except?: Position): TPromise<void>;
getActiveEditor(): BaseEditor;
getVisibleEditors(): IEditor[];
getActiveEditorInput(): IEditorInput;
}
type ICachedEditorInput = ResourceEditorInput | IFileEditorInput;
export class WorkbenchEditorService implements IWorkbenchEditorService {
public _serviceBrand: any;
private static CACHE: ResourceMap<ICachedEditorInput> = new ResourceMap<ICachedEditorInput>();
private editorPart: IEditorPart | IWorkbenchEditorService;
private fileInputFactory: IFileInputFactory;
constructor(
editorPart: IEditorPart | IWorkbenchEditorService,
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
@IInstantiationService private instantiationService: IInstantiationService,
@IEnvironmentService private environmentService: IEnvironmentService
) {
this.editorPart = editorPart;
this.fileInputFactory = Registry.as<IEditorRegistry>(Extensions.Editors).getFileInputFactory();
}
public getActiveEditor(): IEditor {
return this.editorPart.getActiveEditor();
}
public getActiveEditorInput(): IEditorInput {
return this.editorPart.getActiveEditorInput();
}
public getVisibleEditors(): IEditor[] {
return this.editorPart.getVisibleEditors();
}
public isVisible(input: IEditorInput, includeSideBySide: boolean): boolean {
if (!input) {
return false;
}
return this.getVisibleEditors().some(editor => {
if (!editor.input) {
return false;
}
if (input.matches(editor.input)) {
return true;
}
if (includeSideBySide && editor.input instanceof SideBySideEditorInput) {
const sideBySideInput = <SideBySideEditorInput>editor.input;
return input.matches(sideBySideInput.master) || input.matches(sideBySideInput.details);
}
return false;
});
}
public openEditor(input: IEditorInput, options?: IEditorOptions, sideBySide?: boolean): TPromise<IEditor>;
public openEditor(input: IEditorInput, options?: IEditorOptions, position?: Position): TPromise<IEditor>;
public openEditor(input: IResourceInputType, position?: Position): TPromise<IEditor>;
public openEditor(input: IResourceInputType, sideBySide?: boolean): TPromise<IEditor>;
public openEditor(input: any, arg2?: any, arg3?: any): TPromise<IEditor> {
if (!input) {
return TPromise.as<IEditor>(null);
}
// Workbench Input Support
if (input instanceof EditorInput) {
return this.doOpenEditor(input, this.toOptions(arg2), arg3);
}
// Support opening foreign resources (such as a http link that points outside of the workbench)
const resourceInput = <IResourceInput>input;
if (resourceInput.resource instanceof URI) {
const schema = resourceInput.resource.scheme;
if (schema === network.Schemas.http || schema === network.Schemas.https) {
window.open(resourceInput.resource.toString(true));
return TPromise.as<IEditor>(null);
}
}
// Untyped Text Editor Support (required for code that uses this service below workbench level)
const textInput = <IResourceInputType>input;
const typedInput = this.createInput(textInput);
if (typedInput) {
return this.doOpenEditor(typedInput, TextEditorOptions.from(textInput), arg2);
}
return TPromise.as<IEditor>(null);
}
private toOptions(options?: IEditorOptions | EditorOptions): EditorOptions {
if (!options || options instanceof EditorOptions) {
return options as EditorOptions;
}
const textOptions: ITextEditorOptions = options;
if (!!textOptions.selection) {
return TextEditorOptions.create(options);
}
return EditorOptions.create(options);
}
/**
* Allow subclasses to implement their own behavior for opening editor (see below).
*/
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<IEditor>;
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, position?: Position): TPromise<IEditor>;
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, arg3?: any): TPromise<IEditor> {
return this.editorPart.openEditor(input, options, arg3);
}
public openEditors(editors: { input: IResourceInputType, position: Position }[]): TPromise<IEditor[]>;
public openEditors(editors: { input: IEditorInput, position: Position, options?: IEditorOptions }[]): TPromise<IEditor[]>;
public openEditors(editors: any[]): TPromise<IEditor[]> {
const inputs = editors.map(editor => this.createInput(editor.input));
const typedInputs: { input: IEditorInput, position: Position, options?: EditorOptions }[] = inputs.map((input, index) => {
const options = editors[index].input instanceof EditorInput ? this.toOptions(editors[index].options) : TextEditorOptions.from(editors[index].input);
return {
input,
options,
position: editors[index].position
};
});
return this.editorPart.openEditors(typedInputs);
}
public replaceEditors(editors: { toReplace: IResourceInputType, replaceWith: IResourceInputType }[], position?: Position): TPromise<IEditor[]>;
public replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions }[], position?: Position): TPromise<IEditor[]>;
public replaceEditors(editors: any[], position?: Position): TPromise<IEditor[]> {
const toReplaceInputs = editors.map(editor => this.createInput(editor.toReplace));
const replaceWithInputs = editors.map(editor => this.createInput(editor.replaceWith));
const typedReplacements: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: EditorOptions }[] = editors.map((editor, index) => {
const options = editor.toReplace instanceof EditorInput ? this.toOptions(editor.options) : TextEditorOptions.from(editor.replaceWith);
return {
toReplace: toReplaceInputs[index],
replaceWith: replaceWithInputs[index],
options
};
});
return this.editorPart.replaceEditors(typedReplacements, position);
}
public closeEditor(position: Position, input: IEditorInput): TPromise<void> {
return this.doCloseEditor(position, input);
}
protected doCloseEditor(position: Position, input: IEditorInput): TPromise<void> {
return this.editorPart.closeEditor(position, input);
}
public closeEditors(position: Position, filter?: { except?: IEditorInput, direction?: Direction, unmodifiedOnly?: boolean }): TPromise<void> {
return this.editorPart.closeEditors(position, filter);
}
public closeAllEditors(except?: Position): TPromise<void> {
return this.editorPart.closeAllEditors(except);
}
public createInput(input: IEditorInput): EditorInput;
public createInput(input: IResourceInputType): EditorInput;
public createInput(input: any): IEditorInput {
// Workbench Input Support
if (input instanceof EditorInput) {
return input;
}
// Side by Side Support
const resourceSideBySideInput = <IResourceSideBySideInput>input;
if (resourceSideBySideInput.masterResource && resourceSideBySideInput.detailResource) {
const masterInput = this.createInput({ resource: resourceSideBySideInput.masterResource });
const detailInput = this.createInput({ resource: resourceSideBySideInput.detailResource });
return new SideBySideEditorInput(resourceSideBySideInput.label || masterInput.getName(), typeof resourceSideBySideInput.description === 'string' ? resourceSideBySideInput.description : masterInput.getDescription(), detailInput, masterInput);
}
// Diff Editor Support
const resourceDiffInput = <IResourceDiffInput>input;
if (resourceDiffInput.leftResource && resourceDiffInput.rightResource) {
const leftInput = this.createInput({ resource: resourceDiffInput.leftResource });
const rightInput = this.createInput({ resource: resourceDiffInput.rightResource });
const label = resourceDiffInput.label || this.toDiffLabel(resourceDiffInput.leftResource, resourceDiffInput.rightResource, this.workspaceContextService, this.environmentService);
return new DiffEditorInput(label, resourceDiffInput.description, leftInput, rightInput);
}
// Untitled file support
const untitledInput = <IUntitledResourceInput>input;
if (!untitledInput.resource || typeof untitledInput.filePath === 'string' || (untitledInput.resource instanceof URI && untitledInput.resource.scheme === UNTITLED_SCHEMA)) {
// {{SQL CARBON EDIT}}
return this.untitledEditorService.createOrGet(untitledInput.filePath ? URI.file(untitledInput.filePath) : untitledInput.resource, 'sql', untitledInput.contents, untitledInput.encoding);
}
const resourceInput = <IResourceInput>input;
// Files support
if (resourceInput.resource instanceof URI && resourceInput.resource.scheme === network.Schemas.file) {
return this.createOrGet(resourceInput.resource, this.instantiationService, resourceInput.label, resourceInput.description, resourceInput.encoding);
}
// Any other resource
else if (resourceInput.resource instanceof URI) {
const label = resourceInput.label || basename(resourceInput.resource.fsPath);
let description: string;
if (typeof resourceInput.description === 'string') {
description = resourceInput.description;
} else if (resourceInput.resource.scheme === network.Schemas.file) {
description = dirname(resourceInput.resource.fsPath);
}
return this.createOrGet(resourceInput.resource, this.instantiationService, label, description);
}
return null;
}
private createOrGet(resource: URI, instantiationService: IInstantiationService, label: string, description: string, encoding?: string): ICachedEditorInput {
if (WorkbenchEditorService.CACHE.has(resource)) {
const input = WorkbenchEditorService.CACHE.get(resource);
if (input instanceof ResourceEditorInput) {
input.setName(label);
input.setDescription(description);
} else {
input.setPreferredEncoding(encoding);
}
return input;
}
let input: ICachedEditorInput;
if (resource.scheme === network.Schemas.file) {
input = this.fileInputFactory.createFileInput(resource, encoding, instantiationService);
} else {
input = instantiationService.createInstance(ResourceEditorInput, label, description, resource);
}
WorkbenchEditorService.CACHE.set(resource, input);
once(input.onDispose)(() => {
WorkbenchEditorService.CACHE.delete(resource);
});
return input;
}
private toDiffLabel(res1: URI, res2: URI, context: IWorkspaceContextService, environment: IEnvironmentService): string {
const leftName = getPathLabel(res1.fsPath, context, environment);
const rightName = getPathLabel(res2.fsPath, context, environment);
return nls.localize('compareLabels', "{0} ↔ {1}", leftName, rightName);
}
}
export interface IEditorOpenHandler {
(input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<BaseEditor>;
(input: IEditorInput, options?: EditorOptions, position?: Position): TPromise<BaseEditor>;
}
export interface IEditorCloseHandler {
(position: Position, input: IEditorInput): TPromise<void>;
}
/**
* Subclass of workbench editor service that delegates all calls to the provided editor service. Subclasses can choose to override the behavior
* of openEditor() and closeEditor() by providing a handler.
*
* This gives clients a chance to override the behavior of openEditor() and closeEditor().
*/
export class DelegatingWorkbenchEditorService extends WorkbenchEditorService {
private editorOpenHandler: IEditorOpenHandler;
private editorCloseHandler: IEditorCloseHandler;
constructor(
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IInstantiationService instantiationService: IInstantiationService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@IWorkbenchEditorService editorService: IWorkbenchEditorService,
@IEnvironmentService environmentService: IEnvironmentService
) {
super(
editorService,
untitledEditorService,
workspaceContextService,
instantiationService,
environmentService
);
}
public setEditorOpenHandler(handler: IEditorOpenHandler): void {
this.editorOpenHandler = handler;
}
public setEditorCloseHandler(handler: IEditorCloseHandler): void {
this.editorCloseHandler = handler;
}
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<IEditor>;
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, position?: Position): TPromise<IEditor>;
protected doOpenEditor(input: IEditorInput, options?: EditorOptions, arg3?: any): TPromise<IEditor> {
const handleOpen = this.editorOpenHandler ? this.editorOpenHandler(input, options, arg3) : TPromise.as(void 0);
return handleOpen.then(editor => {
if (editor) {
return TPromise.as<BaseEditor>(editor);
}
return super.doOpenEditor(input, options, arg3);
});
}
protected doCloseEditor(position: Position, input: IEditorInput): TPromise<void> {
const handleClose = this.editorCloseHandler ? this.editorCloseHandler(position, input) : TPromise.as(void 0);
return handleClose.then(() => {
return super.doCloseEditor(position, input);
});
}
}

View File

@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IEditorService, IEditor, IEditorInput, IEditorOptions, ITextEditorOptions, Position, Direction, IResourceInput, IResourceDiffInput, IResourceSideBySideInput, IUntitledResourceInput } from 'vs/platform/editor/common/editor';
export const IWorkbenchEditorService = createDecorator<IWorkbenchEditorService>('editorService');
export type IResourceInputType = IResourceInput | IUntitledResourceInput | IResourceDiffInput | IResourceSideBySideInput;
/**
* The editor service allows to open editors and work on the active
* editor input and models.
*/
export interface IWorkbenchEditorService extends IEditorService {
_serviceBrand: ServiceIdentifier<any>;
/**
* Returns the currently active editor or null if none.
*/
getActiveEditor(): IEditor;
/**
* Returns the currently active editor input or null if none.
*/
getActiveEditorInput(): IEditorInput;
/**
* Returns an array of visible editors.
*/
getVisibleEditors(): IEditor[];
/**
* Returns if the provided input is currently visible.
*
* @param includeDiff if set to true, will also consider diff editors to find out if the provided
* input is opened either on the left or right hand side of the diff editor.
*/
isVisible(input: IEditorInput, includeDiff: boolean): boolean;
/**
* Opens an Editor on the given input with the provided options at the given position. If sideBySide parameter
* is provided, causes the editor service to decide in what position to open the input.
*/
openEditor(input: IEditorInput, options?: IEditorOptions | ITextEditorOptions, position?: Position): TPromise<IEditor>;
openEditor(input: IEditorInput, options?: IEditorOptions | ITextEditorOptions, sideBySide?: boolean): TPromise<IEditor>;
/**
* Specific overload to open an instance of IResourceInput, IResourceDiffInput or IResourceSideBySideInput.
*/
openEditor(input: IResourceInputType, position?: Position): TPromise<IEditor>;
openEditor(input: IResourceInputType, sideBySide?: boolean): TPromise<IEditor>;
/**
* Similar to #openEditor() but allows to open multiple editors for different positions at the same time. If there are
* more than one editor per position, only the first one will be active and the others stacked behind inactive.
*/
openEditors(editors: { input: IResourceInputType, position: Position }[]): TPromise<IEditor[]>;
openEditors(editors: { input: IEditorInput, position: Position, options?: IEditorOptions | ITextEditorOptions }[]): TPromise<IEditor[]>;
/**
* Given a list of editors to replace, will look across all groups where this editor is open (active or hidden)
* and replace it with the new editor and the provied options.
*/
replaceEditors(editors: { toReplace: IResourceInputType, replaceWith: IResourceInputType }[], position?: Position): TPromise<IEditor[]>;
replaceEditors(editors: { toReplace: IEditorInput, replaceWith: IEditorInput, options?: IEditorOptions | ITextEditorOptions }[], position?: Position): TPromise<IEditor[]>;
/**
* Closes the editor at the provided position.
*/
closeEditor(position: Position, input: IEditorInput): TPromise<void>;
/**
* Closes editors of a specific group at the provided position. If the optional editor is provided to exclude, it
* will not be closed. The direction can be used in that case to control if all other editors should get closed,
* or towards a specific direction.
*/
closeEditors(position: Position, filter?: { except?: IEditorInput, direction?: Direction, unmodifiedOnly?: boolean }): TPromise<void>;
/**
* Closes all editors across all groups. The optional position allows to keep one group alive.
*/
closeAllEditors(except?: Position): TPromise<void>;
/**
* Allows to resolve an untyped input to a workbench typed instanceof editor input
*/
createInput(input: IResourceInputType): IEditorInput;
}

View File

@@ -0,0 +1,282 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import paths = require('vs/base/common/paths');
import { Position, Direction, IEditor } from 'vs/platform/editor/common/editor';
import URI from 'vs/base/common/uri';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions, TextEditorOptions } from 'vs/workbench/common/editor';
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices';
import { DelegatingWorkbenchEditorService, WorkbenchEditorService, IEditorPart } from 'vs/workbench/services/editor/browser/editorService';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
let activeEditor: BaseEditor = <any>{
getSelection: function () {
return 'test.selection';
}
};
let openedEditorInput;
let openedEditorOptions;
let openedEditorPosition;
function toResource(path) {
return URI.from({ scheme: 'custom', path });
}
function toFileResource(path) {
return URI.file(paths.join('C:\\', new Buffer(this.test.fullTitle()).toString('base64'), path));
}
class TestEditorPart implements IEditorPart {
private activeInput;
public getId(): string {
return null;
}
public openEditors(args: any[]): Promise {
return TPromise.as([]);
}
public replaceEditors(editors: { toReplace: EditorInput, replaceWith: EditorInput, options?: any }[]): TPromise<BaseEditor[]> {
return TPromise.as([]);
}
public closeEditors(position: Position, filter?: { except?: EditorInput, direction?: Direction, unmodifiedOnly?: boolean }): TPromise<void> {
return TPromise.as(null);
}
public closeAllEditors(except?: Position): TPromise<void> {
return TPromise.as(null);
}
public closeEditor(position: Position, input: EditorInput): TPromise<void> {
return TPromise.as(null);
}
public openEditor(input?: EditorInput, options?: EditorOptions, sideBySide?: boolean): TPromise<BaseEditor>;
public openEditor(input?: EditorInput, options?: EditorOptions, position?: Position): TPromise<BaseEditor>;
public openEditor(input?: EditorInput, options?: EditorOptions, arg?: any): TPromise<BaseEditor> {
openedEditorInput = input;
openedEditorOptions = options;
openedEditorPosition = arg;
return TPromise.as(activeEditor);
}
public getActiveEditor(): BaseEditor {
return activeEditor;
}
public setActiveEditorInput(input: EditorInput) {
this.activeInput = input;
}
public getActiveEditorInput(): EditorInput {
return this.activeInput;
}
public getVisibleEditors(): IEditor[] {
return [activeEditor];
}
}
suite('WorkbenchEditorService', () => {
test('basics', function () {
let instantiationService = workbenchInstantiationService();
let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource.call(this, '/something.js'), void 0);
let testEditorPart = new TestEditorPart();
testEditorPart.setActiveEditorInput(activeInput);
let service: WorkbenchEditorService = <any>instantiationService.createInstance(<any>WorkbenchEditorService, testEditorPart);
assert.strictEqual(service.getActiveEditor(), activeEditor);
assert.strictEqual(service.getActiveEditorInput(), activeInput);
// Open EditorInput
service.openEditor(activeInput, null).then((editor) => {
assert.strictEqual(openedEditorInput, activeInput);
assert.strictEqual(openedEditorOptions, null);
assert.strictEqual(editor, activeEditor);
assert.strictEqual(service.getVisibleEditors().length, 1);
assert(service.getVisibleEditors()[0] === editor);
});
service.openEditor(activeInput, null, Position.ONE).then((editor) => {
assert.strictEqual(openedEditorInput, activeInput);
assert.strictEqual(openedEditorOptions, null);
assert.strictEqual(editor, activeEditor);
assert.strictEqual(service.getVisibleEditors().length, 1);
assert(service.getVisibleEditors()[0] === editor);
});
// Open Untyped Input (file)
service.openEditor({ resource: toFileResource.call(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof FileEditorInput);
let contentInput = <FileEditorInput>openedEditorInput;
assert.strictEqual(contentInput.getResource().fsPath, toFileResource.call(this, '/index.html').fsPath);
assert(openedEditorOptions instanceof TextEditorOptions);
let textEditorOptions = <TextEditorOptions>openedEditorOptions;
assert(textEditorOptions.hasOptionsDefined());
});
// Open Untyped Input (file, encoding)
service.openEditor({ resource: toFileResource.call(this, '/index.html'), encoding: 'utf16le', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof FileEditorInput);
let contentInput = <FileEditorInput>openedEditorInput;
assert.equal(contentInput.getPreferredEncoding(), 'utf16le');
});
// Open Untyped Input (untitled)
service.openEditor({ options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof UntitledEditorInput);
assert(openedEditorOptions instanceof TextEditorOptions);
let textEditorOptions = <TextEditorOptions>openedEditorOptions;
assert(textEditorOptions.hasOptionsDefined());
});
// Open Untyped Input (untitled with contents)
service.openEditor({ contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof UntitledEditorInput);
const untitledInput = openedEditorInput as UntitledEditorInput;
untitledInput.resolve().then(model => {
assert.equal(model.getValue(), 'Hello Untitled');
});
});
// Open Untyped Input (untitled with file path)
service.openEditor({ filePath: '/some/path.txt', options: { selection: { startLineNumber: 1, startColumn: 1 } } }).then((editor) => {
assert.strictEqual(editor, activeEditor);
assert(openedEditorInput instanceof UntitledEditorInput);
const untitledInput = openedEditorInput as UntitledEditorInput;
assert.ok(untitledInput.hasAssociatedFilePath);
});
});
test('caching', function () {
let instantiationService = workbenchInstantiationService();
let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource.call(this, '/something.js'), void 0);
let testEditorPart = new TestEditorPart();
testEditorPart.setActiveEditorInput(activeInput);
let service: WorkbenchEditorService = <any>instantiationService.createInstance(<any>WorkbenchEditorService, testEditorPart);
// Cached Input (Files)
const fileResource1 = toFileResource.call(this, '/foo/bar/cache1.js');
const fileInput1 = service.createInput({ resource: fileResource1 });
assert.ok(fileInput1);
const fileResource2 = toFileResource.call(this, '/foo/bar/cache2.js');
const fileInput2 = service.createInput({ resource: fileResource2 });
assert.ok(fileInput2);
assert.notEqual(fileInput1, fileInput2);
const fileInput1Again = service.createInput({ resource: fileResource1 });
assert.equal(fileInput1Again, fileInput1);
fileInput1Again.dispose();
assert.ok(fileInput1.isDisposed());
const fileInput1AgainAndAgain = service.createInput({ resource: fileResource1 });
assert.notEqual(fileInput1AgainAndAgain, fileInput1);
assert.ok(!fileInput1AgainAndAgain.isDisposed());
// Cached Input (Resource)
const resource1 = toResource.call(this, '/foo/bar/cache1.js');
const input1 = service.createInput({ resource: resource1 });
assert.ok(input1);
const resource2 = toResource.call(this, '/foo/bar/cache2.js');
const input2 = service.createInput({ resource: resource2 });
assert.ok(input2);
assert.notEqual(input1, input2);
const input1Again = service.createInput({ resource: resource1 });
assert.equal(input1Again, input1);
input1Again.dispose();
assert.ok(input1.isDisposed());
const input1AgainAndAgain = service.createInput({ resource: resource1 });
assert.notEqual(input1AgainAndAgain, input1);
assert.ok(!input1AgainAndAgain.isDisposed());
});
test('delegate', function (done) {
let instantiationService = workbenchInstantiationService();
let activeInput: EditorInput = instantiationService.createInstance(FileEditorInput, toFileResource.call(this, '/something.js'), void 0);
let testEditorPart = new TestEditorPart();
testEditorPart.setActiveEditorInput(activeInput);
instantiationService.createInstance(<any>WorkbenchEditorService, testEditorPart);
class MyEditor extends BaseEditor {
constructor(id: string) {
super(id, null, new TestThemeService());
}
getId(): string {
return 'myEditor';
}
public layout(): void {
}
public createEditor(): any {
}
}
let ed = instantiationService.createInstance(MyEditor, 'my.editor');
let inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource'));
let delegate = instantiationService.createInstance(DelegatingWorkbenchEditorService);
delegate.setEditorOpenHandler((input, options?) => {
assert.strictEqual(input, inp);
return TPromise.as(ed);
});
delegate.setEditorCloseHandler((position, input) => {
assert.strictEqual(input, inp);
done();
return TPromise.as(void 0);
});
delegate.openEditor(inp);
delegate.closeEditor(0, inp);
});
});

View File

@@ -0,0 +1,500 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { stringify } from 'vs/base/common/marshalling';
import * as objects from 'vs/base/common/objects';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { isWindows, isLinux } from 'vs/base/common/platform';
import { findFreePort } from 'vs/base/node/ports';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { ILifecycleService, ShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle';
import { IWindowsService, IWindowService } from 'vs/platform/windows/common/windows';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ChildProcess, fork } from 'child_process';
import { ipcRenderer as ipc } from 'electron';
import product from 'vs/platform/node/product';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { generateRandomPipeName, Protocol } from 'vs/base/parts/ipc/node/ipc.net';
import { createServer, Server, Socket } from 'net';
import Event, { Emitter, debounceEvent, mapEvent, any } from 'vs/base/common/event';
import { fromEventEmitter } from 'vs/base/node/event';
import { IInitData, IWorkspaceData } from 'vs/workbench/api/node/extHost.protocol';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { ICrashReporterService } from 'vs/workbench/services/crashReporter/common/crashReporterService';
import { IBroadcastService, IBroadcast } from 'vs/platform/broadcast/electron-browser/broadcastService';
import { isEqual } from 'vs/base/common/paths';
import { EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, ILogEntry, EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
export class ExtensionHostProcessWorker {
private _onCrashed: Emitter<[number, string]> = new Emitter<[number, string]>();
public readonly onCrashed: Event<[number, string]> = this._onCrashed.event;
private readonly _toDispose: IDisposable[];
private readonly _isExtensionDevHost: boolean;
private readonly _isExtensionDevDebug: boolean;
private readonly _isExtensionDevDebugBrk: boolean;
private readonly _isExtensionDevTestFromCli: boolean;
// State
private _lastExtensionHostError: string;
private _terminating: boolean;
// Resources, in order they get acquired/created when .start() is called:
private _namedPipeServer: Server;
private _extensionHostProcess: ChildProcess;
private _extensionHostConnection: Socket;
private _messageProtocol: TPromise<IMessagePassingProtocol>;
constructor(
/* intentionally not injected */private readonly _extensionService: IExtensionService,
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
@IMessageService private readonly _messageService: IMessageService,
@IWindowsService private readonly _windowsService: IWindowsService,
@IWindowService private readonly _windowService: IWindowService,
@IBroadcastService private readonly _broadcastService: IBroadcastService,
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@IWorkspaceConfigurationService private readonly _configurationService: IWorkspaceConfigurationService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ICrashReporterService private readonly _crashReporterService: ICrashReporterService
) {
// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
this._isExtensionDevHost = this._environmentService.isExtensionDevelopment;
this._isExtensionDevDebug = (typeof this._environmentService.debugExtensionHost.port === 'number');
this._isExtensionDevDebugBrk = !!this._environmentService.debugExtensionHost.break;
this._isExtensionDevTestFromCli = this._isExtensionDevHost && !!this._environmentService.extensionTestsPath && !this._environmentService.debugExtensionHost.break;
this._lastExtensionHostError = null;
this._terminating = false;
this._namedPipeServer = null;
this._extensionHostProcess = null;
this._extensionHostConnection = null;
this._messageProtocol = null;
this._toDispose = [];
this._toDispose.push(this._onCrashed);
this._toDispose.push(this._lifecycleService.onWillShutdown((e) => this._onWillShutdown(e)));
this._toDispose.push(this._lifecycleService.onShutdown(reason => this.terminate()));
this._toDispose.push(this._broadcastService.onBroadcast(b => this._onBroadcast(b)));
const globalExitListener = () => this.terminate();
process.once('exit', globalExitListener);
this._toDispose.push({
dispose: () => {
process.removeListener('exit', globalExitListener);
}
});
}
public dispose(): void {
this.terminate();
}
private _onBroadcast(broadcast: IBroadcast): void {
// Close Ext Host Window Request
if (broadcast.channel === EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL && this._isExtensionDevHost) {
const extensionPaths = broadcast.payload as string[];
if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) {
this._windowService.closeWindow();
}
}
if (broadcast.channel === EXTENSION_RELOAD_BROADCAST_CHANNEL && this._isExtensionDevHost) {
const extensionPaths = broadcast.payload as string[];
if (Array.isArray(extensionPaths) && extensionPaths.some(path => isEqual(this._environmentService.extensionDevelopmentPath, path, !isLinux))) {
this._windowService.reloadWindow();
}
}
}
public start(): TPromise<IMessagePassingProtocol> {
if (this._terminating) {
// .terminate() was called
return null;
}
if (!this._messageProtocol) {
this._messageProtocol = TPromise.join<any>([this._tryListenOnPipe(), this._tryFindDebugPort()]).then((data: [string, number]) => {
const pipeName = data[0];
// The port will be 0 if there's no need to debug or if a free port was not found
const port = data[1];
const opts = {
env: objects.mixin(objects.clone(process.env), {
AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: true,
VSCODE_WINDOW_ID: String(this._windowService.getCurrentWindowId()),
VSCODE_IPC_HOOK_EXTHOST: pipeName,
VSCODE_HANDLES_UNCAUGHT_ERRORS: true,
ELECTRON_NO_ASAR: '1'
}),
// We only detach the extension host on windows. Linux and Mac orphan by default
// and detach under Linux and Mac create another process group.
// We detach because we have noticed that when the renderer exits, its child processes
// (i.e. extension host) are taken down in a brutal fashion by the OS
detached: !!isWindows,
execArgv: port
? ['--nolazy', (this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + port]
: undefined,
silent: true
};
const crashReporterOptions = this._crashReporterService.getChildProcessStartOptions('extensionHost');
if (crashReporterOptions) {
opts.env.CRASH_REPORTER_START_OPTIONS = JSON.stringify(crashReporterOptions);
}
// Run Extension Host as fork of current process
this._extensionHostProcess = fork(URI.parse(require.toUrl('bootstrap')).fsPath, ['--type=extensionHost'], opts);
// Catch all output coming from the extension host process
type Output = { data: string, format: string[] };
this._extensionHostProcess.stdout.setEncoding('utf8');
this._extensionHostProcess.stderr.setEncoding('utf8');
const onStdout = fromEventEmitter<string>(this._extensionHostProcess.stdout, 'data');
const onStderr = fromEventEmitter<string>(this._extensionHostProcess.stderr, 'data');
const onOutput = any(
mapEvent(onStdout, o => ({ data: `%c${o}`, format: [''] })),
mapEvent(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
);
// Debounce all output, so we can render it in the Chrome console as a group
const onDebouncedOutput = debounceEvent<Output>(onOutput, (r, o) => {
return r
? { data: r.data + o.data, format: [...r.format, ...o.format] }
: { data: o.data, format: o.format };
}, 100);
// Print out extension host output
onDebouncedOutput(data => {
console.group('Extension Host');
console.log(data.data, ...data.format);
console.groupEnd();
});
// Support logging from extension host
this._extensionHostProcess.on('message', msg => {
if (msg && (<ILogEntry>msg).type === '__$console') {
this._logExtensionHostMessage(<ILogEntry>msg);
}
});
// Lifecycle
this._extensionHostProcess.on('error', (err) => this._onExtHostProcessError(err));
this._extensionHostProcess.on('exit', (code: number, signal: string) => this._onExtHostProcessExit(code, signal));
// Notify debugger that we are ready to attach to the process if we run a development extension
if (this._isExtensionDevHost && port) {
this._broadcastService.broadcast({
channel: EXTENSION_ATTACH_BROADCAST_CHANNEL,
payload: {
debugId: this._environmentService.debugExtensionHost.debugId,
port
}
});
}
// Help in case we fail to start it
let startupTimeoutHandle: number;
if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
startupTimeoutHandle = setTimeout(() => {
const msg = this._isExtensionDevDebugBrk
? nls.localize('extensionHostProcess.startupFailDebug', "Extension host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.")
: nls.localize('extensionHostProcess.startupFail', "Extension host did not start in 10 seconds, that might be a problem.");
this._messageService.show(Severity.Warning, msg);
}, 10000);
}
// Initialize extension host process with hand shakes
return this._tryExtHostHandshake().then((protocol) => {
clearTimeout(startupTimeoutHandle);
return protocol;
});
});
}
return this._messageProtocol;
}
/**
* Start a server (`this._namedPipeServer`) that listens on a named pipe and return the named pipe name.
*/
private _tryListenOnPipe(): TPromise<string> {
return new TPromise<string>((resolve, reject) => {
const pipeName = generateRandomPipeName();
this._namedPipeServer = createServer();
this._namedPipeServer.on('error', reject);
this._namedPipeServer.listen(pipeName, () => {
this._namedPipeServer.removeListener('error', reject);
resolve(pipeName);
});
});
}
/**
* Find a free port if extension host debugging is enabled.
*/
private _tryFindDebugPort(): TPromise<number> {
const extensionHostPort = this._environmentService.debugExtensionHost.port;
if (typeof extensionHostPort !== 'number') {
return TPromise.wrap<number>(0);
}
return new TPromise<number>((c, e) => {
findFreePort(extensionHostPort, 10 /* try 10 ports */, 5000 /* try up to 5 seconds */, (port) => {
if (!port) {
console.warn('%c[Extension Host] %cCould not find a free port for debugging', 'color: blue', 'color: black');
return c(void 0);
}
if (port !== extensionHostPort) {
console.warn(`%c[Extension Host] %cProvided debugging port ${extensionHostPort} is not free, using ${port} instead.`, 'color: blue', 'color: black');
}
if (this._isExtensionDevDebugBrk) {
console.warn(`%c[Extension Host] %cSTOPPED on first line for debugging on port ${port}`, 'color: blue', 'color: black');
} else {
console.info(`%c[Extension Host] %cdebugger listening on port ${port}`, 'color: blue', 'color: black');
}
return c(port);
});
});
}
private _tryExtHostHandshake(): TPromise<IMessagePassingProtocol> {
return new TPromise<IMessagePassingProtocol>((resolve, reject) => {
// Wait for the extension host to connect to our named pipe
// and wrap the socket in the message passing protocol
let handle = setTimeout(() => {
this._namedPipeServer.close();
this._namedPipeServer = null;
reject('timeout');
}, 60 * 1000);
this._namedPipeServer.on('connection', socket => {
clearTimeout(handle);
this._namedPipeServer.close();
this._namedPipeServer = null;
this._extensionHostConnection = socket;
resolve(new Protocol(this._extensionHostConnection));
});
}).then((protocol) => {
// 1) wait for the incoming `ready` event and send the initialization data.
// 2) wait for the incoming `initialized` event.
return new TPromise<IMessagePassingProtocol>((resolve, reject) => {
let handle = setTimeout(() => {
reject('timeout');
}, 60 * 1000);
const disposable = protocol.onMessage(msg => {
if (msg === 'ready') {
// 1) Extension Host is ready to receive messages, initialize it
this._createExtHostInitData().then(data => protocol.send(stringify(data)));
return;
}
if (msg === 'initialized') {
// 2) Extension Host is initialized
clearTimeout(handle);
// stop listening for messages here
disposable.dispose();
// release this promise
resolve(protocol);
return;
}
console.error(`received unexpected message during handshake phase from the extension host: `, msg);
});
});
});
}
private _createExtHostInitData(): TPromise<IInitData> {
return TPromise.join<any>([this._telemetryService.getTelemetryInfo(), this._extensionService.getExtensions()]).then(([telemetryInfo, extensionDescriptions]) => {
const r: IInitData = {
parentPid: process.pid,
environment: {
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
appRoot: this._environmentService.appRoot,
appSettingsHome: this._environmentService.appSettingsHome,
disableExtensions: this._environmentService.disableExtensions,
userExtensionsHome: this._environmentService.extensionsPath,
extensionDevelopmentPath: this._environmentService.extensionDevelopmentPath,
extensionTestsPath: this._environmentService.extensionTestsPath,
// globally disable proposed api when built and not insiders developing extensions
enableProposedApiForAll: !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0),
enableProposedApiFor: this._environmentService.args['enable-proposed-api'] || []
},
workspace: <IWorkspaceData>this._contextService.getWorkspace(),
extensions: extensionDescriptions,
configuration: this._configurationService.getConfigurationData(),
telemetryInfo
};
return r;
});
}
private _logExtensionHostMessage(logEntry: ILogEntry) {
let args = [];
try {
let parsed = JSON.parse(logEntry.arguments);
args.push(...Object.getOwnPropertyNames(parsed).map(o => parsed[o]));
} catch (error) {
args.push(logEntry.arguments);
}
// If the first argument is a string, check for % which indicates that the message
// uses substitution for variables. In this case, we cannot just inject our colored
// [Extension Host] to the front because it breaks substitution.
let consoleArgs = [];
if (typeof args[0] === 'string' && args[0].indexOf('%') >= 0) {
consoleArgs = [`%c[Extension Host]%c ${args[0]}`, 'color: blue', 'color: black', ...args.slice(1)];
} else {
consoleArgs = ['%c[Extension Host]', 'color: blue', ...args];
}
// Send to local console unless we run tests from cli
if (!this._isExtensionDevTestFromCli) {
console[logEntry.severity].apply(console, consoleArgs);
}
// Log on main side if running tests from cli
if (this._isExtensionDevTestFromCli) {
this._windowsService.log(logEntry.severity, ...args);
}
// Broadcast to other windows if we are in development mode
else if (!this._environmentService.isBuilt || this._isExtensionDevHost) {
this._broadcastService.broadcast({
channel: EXTENSION_LOG_BROADCAST_CHANNEL,
payload: {
logEntry,
debugId: this._environmentService.debugExtensionHost.debugId
}
});
}
}
private _onExtHostProcessError(err: any): void {
let errorMessage = toErrorMessage(err);
if (errorMessage === this._lastExtensionHostError) {
return; // prevent error spam
}
this._lastExtensionHostError = errorMessage;
this._messageService.show(Severity.Error, nls.localize('extensionHostProcess.error', "Error from the extension host: {0}", errorMessage));
}
private _onExtHostProcessExit(code: number, signal: string): void {
if (this._terminating) {
// Expected termination path (we asked the process to terminate)
return;
}
// Unexpected termination
if (!this._isExtensionDevHost) {
this._onCrashed.fire([code, signal]);
}
// Expected development extension termination: When the extension host goes down we also shutdown the window
else if (!this._isExtensionDevTestFromCli) {
this._windowService.closeWindow();
}
// When CLI testing make sure to exit with proper exit code
else {
ipc.send('vscode:exit', code);
}
}
public terminate(): void {
if (this._terminating) {
return;
}
this._terminating = true;
dispose(this._toDispose);
if (!this._messageProtocol) {
// .start() was not called
return;
}
this._messageProtocol.then((protocol) => {
// Send the extension host a request to terminate itself
// (graceful termination)
protocol.send({
type: '__$terminate'
});
// Give the extension host 60s, after which we will
// try to kill the process and release any resources
setTimeout(() => this._cleanResources(), 60 * 1000);
}, (err) => {
// Establishing a protocol with the extension host failed, so
// try to kill the process and release any resources.
this._cleanResources();
});
}
private _cleanResources(): void {
if (this._namedPipeServer) {
this._namedPipeServer.close();
this._namedPipeServer = null;
}
if (this._extensionHostConnection) {
this._extensionHostConnection.end();
this._extensionHostConnection = null;
}
if (this._extensionHostProcess) {
this._extensionHostProcess.kill();
this._extensionHostProcess = null;
}
}
private _onWillShutdown(event: ShutdownEvent): void {
// If the extension development host was started without debugger attached we need
// to communicate this back to the main side to terminate the debug session
if (this._isExtensionDevHost && !this._isExtensionDevTestFromCli && !this._isExtensionDevDebug) {
this._broadcastService.broadcast({
channel: EXTENSION_TERMINATE_BROADCAST_CHANNEL,
payload: {
debugId: this._environmentService.debugExtensionHost.debugId
}
});
event.veto(TPromise.timeout(100 /* wait a bit for IPC to get delivered */).then(() => false));
}
}
}

View File

@@ -0,0 +1,363 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import * as Platform from 'vs/base/common/platform';
import pfs = require('vs/base/node/pfs');
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { TPromise } from 'vs/base/common/winjs.base';
import { groupBy, values } from 'vs/base/common/collections';
import { join, normalize, extname } from 'path';
import json = require('vs/base/common/json');
import Types = require('vs/base/common/types');
import { isValidExtensionDescription } from 'vs/platform/extensions/node/extensionValidator';
import * as semver from 'semver';
import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
const MANIFEST_FILE = 'package.json';
const devMode = !!process.env['VSCODE_DEV'];
interface NlsConfiguration {
locale: string;
pseudo: boolean;
}
const nlsConfig: NlsConfiguration = {
locale: Platform.locale,
pseudo: Platform.locale === 'pseudo'
};
export interface ILog {
error(source: string, message: string): void;
warn(source: string, message: string): void;
info(source: string, message: string): void;
}
abstract class ExtensionManifestHandler {
protected _ourVersion: string;
protected _log: ILog;
protected _absoluteFolderPath: string;
protected _isBuiltin: boolean;
protected _absoluteManifestPath: string;
constructor(ourVersion: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean) {
this._ourVersion = ourVersion;
this._log = log;
this._absoluteFolderPath = absoluteFolderPath;
this._isBuiltin = isBuiltin;
this._absoluteManifestPath = join(absoluteFolderPath, MANIFEST_FILE);
}
}
class ExtensionManifestParser extends ExtensionManifestHandler {
public parse(): TPromise<IExtensionDescription> {
return pfs.readFile(this._absoluteManifestPath).then((manifestContents) => {
try {
return JSON.parse(manifestContents.toString());
} catch (e) {
this._log.error(this._absoluteFolderPath, nls.localize('jsonParseFail', "Failed to parse {0}: {1}.", this._absoluteManifestPath, getParseErrorMessage(e.message)));
}
return null;
}, (err) => {
if (err.code === 'ENOENT') {
return null;
}
this._log.error(this._absoluteFolderPath, nls.localize('fileReadFail', "Cannot read file {0}: {1}.", this._absoluteManifestPath, err.message));
return null;
});
}
}
class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
public replaceNLS(extensionDescription: IExtensionDescription): TPromise<IExtensionDescription> {
let extension = extname(this._absoluteManifestPath);
let basename = this._absoluteManifestPath.substr(0, this._absoluteManifestPath.length - extension.length);
return pfs.fileExists(basename + '.nls' + extension).then(exists => {
if (!exists) {
return extensionDescription;
}
return ExtensionManifestNLSReplacer.findMessageBundles(basename).then((messageBundle) => {
if (!messageBundle.localized) {
return extensionDescription;
}
return pfs.readFile(messageBundle.localized).then(messageBundleContent => {
let errors: json.ParseError[] = [];
let messages: { [key: string]: string; } = json.parse(messageBundleContent.toString(), errors);
return ExtensionManifestNLSReplacer.resolveOriginalMessageBundle(messageBundle.original, errors).then(originalMessages => {
if (errors.length > 0) {
errors.forEach((error) => {
this._log.error(this._absoluteFolderPath, nls.localize('jsonsParseFail', "Failed to parse {0} or {1}: {2}.", messageBundle.localized, messageBundle.original, getParseErrorMessage(error.error)));
});
return extensionDescription;
}
ExtensionManifestNLSReplacer._replaceNLStrings(extensionDescription, messages, originalMessages, this._log, this._absoluteFolderPath);
return extensionDescription;
});
}, (err) => {
this._log.error(this._absoluteFolderPath, nls.localize('fileReadFail', "Cannot read file {0}: {1}.", messageBundle.localized, err.message));
return null;
});
});
});
}
/**
* Parses original message bundle, returns null if the original message bundle is null.
*/
private static resolveOriginalMessageBundle(originalMessageBundle: string, errors: json.ParseError[]) {
return new TPromise<{ [key: string]: string; }>((c, e, p) => {
if (originalMessageBundle) {
pfs.readFile(originalMessageBundle).then(originalBundleContent => {
c(json.parse(originalBundleContent.toString(), errors));
});
} else {
c(null);
}
});
}
/**
* Finds localized message bundle and the original (unlocalized) one.
* If the localized file is not present, returns null for the original and marks original as localized.
*/
private static findMessageBundles(basename: string): TPromise<{ localized: string, original: string }> {
return new TPromise<{ localized: string, original: string }>((c, e, p) => {
function loop(basename: string, locale: string): void {
let toCheck = `${basename}.nls.${locale}.json`;
pfs.fileExists(toCheck).then(exists => {
if (exists) {
c({ localized: toCheck, original: `${basename}.nls.json` });
}
let index = locale.lastIndexOf('-');
if (index === -1) {
c({ localized: `${basename}.nls.json`, original: null });
} else {
locale = locale.substring(0, index);
loop(basename, locale);
}
});
}
if (devMode || nlsConfig.pseudo || !nlsConfig.locale) {
return c({ localized: basename + '.nls.json', original: null });
}
loop(basename, nlsConfig.locale);
});
}
/**
* This routine makes the following assumptions:
* The root element is an object literal
*/
private static _replaceNLStrings<T>(literal: T, messages: { [key: string]: string; }, originalMessages: { [key: string]: string }, log: ILog, messageScope: string): void {
function processEntry(obj: any, key: string | number, command?: boolean) {
let value = obj[key];
if (Types.isString(value)) {
let str = <string>value;
let length = str.length;
if (length > 1 && str[0] === '%' && str[length - 1] === '%') {
let messageKey = str.substr(1, length - 2);
let message = messages[messageKey];
if (message) {
if (nlsConfig.pseudo) {
// FF3B and FF3D is the Unicode zenkaku representation for [ and ]
message = '\uFF3B' + message.replace(/[aouei]/g, '$&$&') + '\uFF3D';
}
obj[key] = command && (key === 'title' || key === 'category') && originalMessages ? { value: message, original: originalMessages[messageKey] } : message;
} else {
log.warn(messageScope, nls.localize('missingNLSKey', "Couldn't find message for key {0}.", messageKey));
}
}
} else if (Types.isObject(value)) {
for (let k in value) {
if (value.hasOwnProperty(k)) {
k === 'commands' ? processEntry(value, k, true) : processEntry(value, k, command);
}
}
} else if (Types.isArray(value)) {
for (let i = 0; i < value.length; i++) {
processEntry(value, i, command);
}
}
}
for (let key in literal) {
if (literal.hasOwnProperty(key)) {
processEntry(literal, key);
}
};
}
}
class ExtensionManifestValidator extends ExtensionManifestHandler {
validate(_extensionDescription: IExtensionDescription): IExtensionDescription {
// Relax the readonly properties here, it is the one place where we check and normalize values
interface IRelaxedExtensionDescription {
id: string;
name: string;
version: string;
publisher: string;
isBuiltin: boolean;
extensionFolderPath: string;
engines: {
vscode: string;
};
main?: string;
enableProposedApi?: boolean;
}
let extensionDescription = <IRelaxedExtensionDescription>_extensionDescription;
extensionDescription.isBuiltin = this._isBuiltin;
let notices: string[] = [];
if (!isValidExtensionDescription(this._ourVersion, this._absoluteFolderPath, extensionDescription, notices)) {
notices.forEach((error) => {
this._log.error(this._absoluteFolderPath, error);
});
return null;
}
// in this case the notices are warnings
notices.forEach((error) => {
this._log.warn(this._absoluteFolderPath, error);
});
// id := `publisher.name`
extensionDescription.id = `${extensionDescription.publisher}.${extensionDescription.name}`;
// main := absolutePath(`main`)
if (extensionDescription.main) {
extensionDescription.main = join(this._absoluteFolderPath, extensionDescription.main);
}
extensionDescription.extensionFolderPath = this._absoluteFolderPath;
return extensionDescription;
}
}
export class ExtensionScanner {
/**
* Read the extension defined in `absoluteFolderPath`
*/
public static scanExtension(
version: string,
log: ILog,
absoluteFolderPath: string,
isBuiltin: boolean
): TPromise<IExtensionDescription> {
absoluteFolderPath = normalize(absoluteFolderPath);
let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin);
return parser.parse().then((extensionDescription) => {
if (extensionDescription === null) {
return null;
}
let nlsReplacer = new ExtensionManifestNLSReplacer(version, log, absoluteFolderPath, isBuiltin);
return nlsReplacer.replaceNLS(extensionDescription);
}).then((extensionDescription) => {
if (extensionDescription === null) {
return null;
}
let validator = new ExtensionManifestValidator(version, log, absoluteFolderPath, isBuiltin);
return validator.validate(extensionDescription);
});
}
/**
* Scan a list of extensions defined in `absoluteFolderPath`
*/
public static scanExtensions(
version: string,
log: ILog,
absoluteFolderPath: string,
isBuiltin: boolean
): TPromise<IExtensionDescription[]> {
let obsolete = TPromise.as({});
if (!isBuiltin) {
obsolete = pfs.readFile(join(absoluteFolderPath, '.obsolete'), 'utf8')
.then(raw => JSON.parse(raw))
.then(null, err => ({}));
}
return obsolete.then(obsolete => {
return pfs.readDirsInDir(absoluteFolderPath)
.then(folders => {
if (isBuiltin) {
return folders;
}
// TODO: align with extensionsService
const nonGallery: string[] = [];
const gallery: { folder: string; id: string; version: string; }[] = [];
folders.forEach(folder => {
if (obsolete[folder]) {
return;
}
const { id, version } = getIdAndVersionFromLocalExtensionId(folder);
if (!id || !version) {
nonGallery.push(folder);
return;
}
gallery.push({ folder, id, version });
});
const byId = values(groupBy(gallery, p => p.id));
const latest = byId.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0])
.map(a => a.folder);
return [...nonGallery, ...latest];
})
.then(folders => TPromise.join(folders.map(f => this.scanExtension(version, log, join(absoluteFolderPath, f), isBuiltin))))
.then(extensionDescriptions => extensionDescriptions.filter(item => item !== null))
.then(null, err => {
log.error(absoluteFolderPath, err);
return [];
});
});
}
/**
* Combination of scanExtension and scanExtensions: If an extension manifest is found at root, we load just this extension,
* otherwise we assume the folder contains multiple extensions.
*/
public static scanOneOrMultipleExtensions(
version: string,
log: ILog,
absoluteFolderPath: string,
isBuiltin: boolean
): TPromise<IExtensionDescription[]> {
return pfs.fileExists(join(absoluteFolderPath, MANIFEST_FILE)).then((exists) => {
if (exists) {
return this.scanExtension(version, log, absoluteFolderPath, isBuiltin).then((extensionDescription) => {
if (extensionDescription === null) {
return [];
}
return [extensionDescription];
});
}
return this.scanExtensions(version, log, absoluteFolderPath, isBuiltin);
}, (err) => {
log.error(absoluteFolderPath, err);
return [];
});
}
}

View File

@@ -0,0 +1,449 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import * as errors from 'vs/base/common/errors';
import Severity from 'vs/base/common/severity';
import { TPromise } from 'vs/base/common/winjs.base';
import pkg from 'vs/platform/node/package';
import * as path from 'path';
import URI from 'vs/base/common/uri';
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry';
import { IMessage, IExtensionDescription, IExtensionsStatus, IExtensionService, ExtensionPointContribution, ActivationTimes } from 'vs/platform/extensions/common/extensions';
import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, getGloballyDisabledExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint } from 'vs/platform/extensions/common/extensionsRegistry';
import { ExtensionScanner, ILog } from 'vs/workbench/services/extensions/electron-browser/extensionPoints';
import { IMessageService } from 'vs/platform/message/common/message';
import { ProxyIdentifier } from 'vs/workbench/services/thread/common/threadService';
import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
import { MainThreadService } from 'vs/workbench/services/thread/electron-browser/threadService';
import { Barrier } from 'vs/workbench/services/extensions/node/barrier';
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { ExtHostCustomersRegistry } from 'vs/workbench/api/electron-browser/extHostCustomers';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { Action } from 'vs/base/common/actions';
import { IDisposable } from 'vs/base/common/lifecycle';
const SystemExtensionsRoot = path.normalize(path.join(URI.parse(require.toUrl('')).fsPath, '..', 'extensions'));
function messageWithSource(msg: IMessage): string {
return messageWithSource2(msg.source, msg.message);
}
function messageWithSource2(source: string, message: string): string {
if (source) {
return `[${source}]: ${message}`;
}
return message;
}
const hasOwnProperty = Object.hasOwnProperty;
const NO_OP_VOID_PROMISE = TPromise.as<void>(void 0);
export class ExtensionService implements IExtensionService {
public _serviceBrand: any;
private _registry: ExtensionDescriptionRegistry;
private readonly _barrier: Barrier;
private readonly _isDev: boolean;
private readonly _extensionsStatus: { [id: string]: IExtensionsStatus };
private _allRequestedActivateEvents: { [activationEvent: string]: boolean; };
// --- Members used per extension host process
/**
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
*/
private _extensionHostProcessFinishedActivateEvents: { [activationEvent: string]: boolean; };
private _extensionHostProcessActivationTimes: { [id: string]: ActivationTimes; };
private _extensionHostProcessWorker: ExtensionHostProcessWorker;
private _extensionHostProcessThreadService: MainThreadService;
private _extensionHostProcessCustomers: IDisposable[];
/**
* winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object.
*/
private _extensionHostProcessProxy: TPromise<{ value: ExtHostExtensionServiceShape; }>;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IMessageService private readonly _messageService: IMessageService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService,
@IStorageService private readonly _storageService: IStorageService,
@IWindowService private readonly _windowService: IWindowService
) {
this._registry = null;
this._barrier = new Barrier();
this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment;
this._extensionsStatus = {};
this._allRequestedActivateEvents = Object.create(null);
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
this._extensionHostProcessActivationTimes = Object.create(null);
this._extensionHostProcessWorker = null;
this._extensionHostProcessThreadService = null;
this._extensionHostProcessCustomers = [];
this._extensionHostProcessProxy = null;
this._startExtensionHostProcess([]);
this._scanAndHandleExtensions();
}
public restartExtensionHost(): void {
this._stopExtensionHostProcess();
this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents));
}
private _stopExtensionHostProcess(): void {
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
this._extensionHostProcessActivationTimes = Object.create(null);
if (this._extensionHostProcessWorker) {
this._extensionHostProcessWorker.dispose();
this._extensionHostProcessWorker = null;
}
if (this._extensionHostProcessThreadService) {
this._extensionHostProcessThreadService.dispose();
this._extensionHostProcessThreadService = null;
}
for (let i = 0, len = this._extensionHostProcessCustomers.length; i < len; i++) {
const customer = this._extensionHostProcessCustomers[i];
try {
customer.dispose();
} catch (err) {
errors.onUnexpectedError(err);
}
}
this._extensionHostProcessCustomers = [];
this._extensionHostProcessProxy = null;
}
private _startExtensionHostProcess(initialActivationEvents: string[]): void {
this._stopExtensionHostProcess();
this._extensionHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this);
this._extensionHostProcessWorker.onCrashed(([code, signal]) => this._onExtensionHostCrashed(code, signal));
this._extensionHostProcessProxy = this._extensionHostProcessWorker.start().then(
(protocol) => {
return { value: this._createExtensionHostCustomers(protocol) };
},
(err) => {
console.error('Error received from starting extension host');
console.error(err);
return null;
}
);
this._extensionHostProcessProxy.then(() => {
initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent));
});
}
private _onExtensionHostCrashed(code: number, signal: string): void {
const openDevTools = new Action('openDevTools', nls.localize('devTools', "Developer Tools"), '', true, (): TPromise<boolean> => {
return this._windowService.openDevTools().then(() => false);
});
const restart = new Action('restart', nls.localize('restart', "Restart Extension Host"), '', true, (): TPromise<boolean> => {
this._messageService.hideAll();
this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents));
return TPromise.as(true);
});
console.error('Extension host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
this._stopExtensionHostProcess();
let message = nls.localize('extensionHostProcess.crash', "Extension host terminated unexpectedly.");
if (code === 87) {
message = nls.localize('extensionHostProcess.unresponsiveCrash', "Extension host terminated because it was not responsive.");
}
this._messageService.show(Severity.Error, {
message: message,
actions: [
openDevTools,
restart
]
});
}
private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape {
this._extensionHostProcessThreadService = this._instantiationService.createInstance(MainThreadService, protocol);
const extHostContext: IExtHostContext = this._extensionHostProcessThreadService;
// Named customers
const namedCustomers = ExtHostCustomersRegistry.getNamedCustomers();
for (let i = 0, len = namedCustomers.length; i < len; i++) {
const [id, ctor] = namedCustomers[i];
const instance = this._instantiationService.createInstance(ctor, extHostContext);
this._extensionHostProcessCustomers.push(instance);
this._extensionHostProcessThreadService.set(id, instance);
}
// Customers
const customers = ExtHostCustomersRegistry.getCustomers();
for (let i = 0, len = customers.length; i < len; i++) {
const ctor = customers[i];
const instance = this._instantiationService.createInstance(ctor, extHostContext);
this._extensionHostProcessCustomers.push(instance);
}
// Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = Object.keys(MainContext).map((key) => MainContext[key]);
this._extensionHostProcessThreadService.assertRegistered(expected);
return this._extensionHostProcessThreadService.get(ExtHostContext.ExtHostExtensionService);
}
// ---- begin IExtensionService
public activateByEvent(activationEvent: string): TPromise<void> {
if (this._barrier.isOpen()) {
// Extensions have been scanned and interpreted
if (!this._registry.containsActivationEvent(activationEvent)) {
// There is no extension that is interested in this activation event
return NO_OP_VOID_PROMISE;
}
// Record the fact that this activationEvent was requested (in case of a restart)
this._allRequestedActivateEvents[activationEvent] = true;
return this._activateByEvent(activationEvent);
} else {
// Extensions have not been scanned yet.
// Record the fact that this activationEvent was requested (in case of a restart)
this._allRequestedActivateEvents[activationEvent] = true;
return this._barrier.wait().then(() => this._activateByEvent(activationEvent));
}
}
protected _activateByEvent(activationEvent: string): TPromise<void> {
if (this._extensionHostProcessFinishedActivateEvents[activationEvent]) {
return NO_OP_VOID_PROMISE;
}
return this._extensionHostProcessProxy.then((proxy) => {
return proxy.value.$activateByEvent(activationEvent);
}).then(() => {
this._extensionHostProcessFinishedActivateEvents[activationEvent] = true;
});
}
public onReady(): TPromise<boolean> {
return this._barrier.wait();
}
public getExtensions(): TPromise<IExtensionDescription[]> {
return this.onReady().then(() => {
return this._registry.getAllExtensionDescriptions();
});
}
public readExtensionPointContributions<T>(extPoint: IExtensionPoint<T>): TPromise<ExtensionPointContribution<T>[]> {
return this.onReady().then(() => {
let availableExtensions = this._registry.getAllExtensionDescriptions();
let result: ExtensionPointContribution<T>[] = [], resultLen = 0;
for (let i = 0, len = availableExtensions.length; i < len; i++) {
let desc = availableExtensions[i];
if (desc.contributes && hasOwnProperty.call(desc.contributes, extPoint.name)) {
result[resultLen++] = new ExtensionPointContribution<T>(desc, desc.contributes[extPoint.name]);
}
}
return result;
});
}
public getExtensionsStatus(): { [id: string]: IExtensionsStatus; } {
return this._extensionsStatus;
}
public getExtensionsActivationTimes(): { [id: string]: ActivationTimes; } {
return this._extensionHostProcessActivationTimes;
}
// ---- end IExtensionService
// --- impl
private _scanAndHandleExtensions(): void {
const log = new Logger((severity, source, message) => {
this._logOrShowMessage(severity, this._isDev ? messageWithSource2(source, message) : message);
});
ExtensionService._scanInstalledExtensions(this._environmentService, log).then((installedExtensions) => {
const disabledExtensions = [
...getGloballyDisabledExtensions(this._extensionEnablementService, this._storageService, installedExtensions),
...this._extensionEnablementService.getWorkspaceDisabledExtensions()
];
this._telemetryService.publicLog('extensionsScanned', {
totalCount: installedExtensions.length,
disabledCount: disabledExtensions.length
});
if (disabledExtensions.length === 0) {
return installedExtensions;
}
return installedExtensions.filter(e => disabledExtensions.every(id => !areSameExtensions({ id }, e)));
}).then((extensionDescriptions) => {
this._registry = new ExtensionDescriptionRegistry(extensionDescriptions);
let availableExtensions = this._registry.getAllExtensionDescriptions();
let extensionPoints = ExtensionsRegistry.getExtensionPoints();
let messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
for (let i = 0, len = extensionPoints.length; i < len; i++) {
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
}
this._barrier.open();
});
}
private _handleExtensionPointMessage(msg: IMessage) {
if (!this._extensionsStatus[msg.source]) {
this._extensionsStatus[msg.source] = { messages: [] };
}
this._extensionsStatus[msg.source].messages.push(msg);
if (msg.source === this._environmentService.extensionDevelopmentPath) {
// This message is about the extension currently being developed
this._showMessageToUser(msg.type, messageWithSource(msg));
} else {
this._logMessageInConsole(msg.type, messageWithSource(msg));
}
if (!this._isDev && msg.extensionId) {
const { type, extensionId, extensionPointId, message } = msg;
this._telemetryService.publicLog('extensionsMessage', {
type, extensionId, extensionPointId, message
});
}
}
private static _scanInstalledExtensions(environmentService: IEnvironmentService, log: ILog): TPromise<IExtensionDescription[]> {
const version = pkg.version;
const builtinExtensions = ExtensionScanner.scanExtensions(version, log, SystemExtensionsRoot, true);
const userExtensions = environmentService.disableExtensions || !environmentService.extensionsPath ? TPromise.as([]) : ExtensionScanner.scanExtensions(version, log, environmentService.extensionsPath, false);
const developedExtensions = environmentService.disableExtensions || !environmentService.isExtensionDevelopment ? TPromise.as([]) : ExtensionScanner.scanOneOrMultipleExtensions(version, log, environmentService.extensionDevelopmentPath, false);
return TPromise.join([builtinExtensions, userExtensions, developedExtensions]).then<IExtensionDescription[]>((extensionDescriptions: IExtensionDescription[][]) => {
const builtinExtensions = extensionDescriptions[0];
const userExtensions = extensionDescriptions[1];
const developedExtensions = extensionDescriptions[2];
let result: { [extensionId: string]: IExtensionDescription; } = {};
builtinExtensions.forEach((builtinExtension) => {
result[builtinExtension.id] = builtinExtension;
});
userExtensions.forEach((userExtension) => {
if (result.hasOwnProperty(userExtension.id)) {
log.warn(userExtension.extensionFolderPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionFolderPath, userExtension.extensionFolderPath));
}
result[userExtension.id] = userExtension;
});
developedExtensions.forEach(developedExtension => {
log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionFolderPath));
if (result.hasOwnProperty(developedExtension.id)) {
log.warn(developedExtension.extensionFolderPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionFolderPath, developedExtension.extensionFolderPath));
}
result[developedExtension.id] = developedExtension;
});
return Object.keys(result).map(name => result[name]);
}).then(null, err => {
log.error('', err);
return [];
});
}
private static _handleExtensionPoint<T>(extensionPoint: ExtensionPoint<T>, availableExtensions: IExtensionDescription[], messageHandler: (msg: IMessage) => void): void {
let users: IExtensionPointUser<T>[] = [], usersLen = 0;
for (let i = 0, len = availableExtensions.length; i < len; i++) {
let desc = availableExtensions[i];
if (desc.contributes && hasOwnProperty.call(desc.contributes, extensionPoint.name)) {
users[usersLen++] = {
description: desc,
value: desc.contributes[extensionPoint.name],
collector: new ExtensionMessageCollector(messageHandler, desc, extensionPoint.name)
};
}
}
extensionPoint.acceptUsers(users);
}
private _showMessageToUser(severity: Severity, msg: string): void {
if (severity === Severity.Error || severity === Severity.Warning) {
this._messageService.show(severity, msg);
} else {
this._logMessageInConsole(severity, msg);
}
}
private _logMessageInConsole(severity: Severity, msg: string): void {
if (severity === Severity.Error) {
console.error(msg);
} else if (severity === Severity.Warning) {
console.warn(msg);
} else {
console.log(msg);
}
}
// -- called by extension host
public _logOrShowMessage(severity: Severity, msg: string): void {
if (this._isDev) {
this._showMessageToUser(severity, msg);
} else {
this._logMessageInConsole(severity, msg);
}
}
public _onExtensionActivated(extensionId: string, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number): void {
this._extensionHostProcessActivationTimes[extensionId] = new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime);
}
}
export class Logger implements ILog {
private readonly _messageHandler: (severity: Severity, source: string, message: string) => void;
constructor(
messageHandler: (severity: Severity, source: string, message: string) => void
) {
this._messageHandler = messageHandler;
}
public error(source: string, message: string): void {
this._messageHandler(Severity.Error, source, message);
}
public warn(source: string, message: string): void {
this._messageHandler(Severity.Warning, source, message);
}
public info(source: string, message: string): void {
this._messageHandler(Severity.Info, source, message);
}
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
/**
* A barrier that is initially closed and then becomes opened permanently.
*/
export class Barrier {
private _isOpen: boolean;
private _promise: TPromise<boolean>;
private _completePromise: (v: boolean) => void;
constructor() {
this._isOpen = false;
this._promise = new TPromise<boolean>((c, e, p) => {
this._completePromise = c;
}, () => {
console.warn('You should really not try to cancel this ready promise!');
});
}
public isOpen(): boolean {
return this._isOpen;
}
public open(): void {
this._isOpen = true;
this._completePromise(true);
}
public wait(): TPromise<boolean> {
return this._promise;
}
}

View File

@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
const hasOwnProperty = Object.hasOwnProperty;
export class ExtensionDescriptionRegistry {
private _extensionsMap: { [extensionId: string]: IExtensionDescription; };
private _extensionsArr: IExtensionDescription[];
private _activationMap: { [activationEvent: string]: IExtensionDescription[]; };
constructor(extensionDescriptions: IExtensionDescription[]) {
this._extensionsMap = {};
this._extensionsArr = [];
this._activationMap = {};
for (let i = 0, len = extensionDescriptions.length; i < len; i++) {
let extensionDescription = extensionDescriptions[i];
if (hasOwnProperty.call(this._extensionsMap, extensionDescription.id)) {
// No overwriting allowed!
console.error('Extension `' + extensionDescription.id + '` is already registered');
continue;
}
this._extensionsMap[extensionDescription.id] = extensionDescription;
this._extensionsArr.push(extensionDescription);
if (Array.isArray(extensionDescription.activationEvents)) {
for (let j = 0, lenJ = extensionDescription.activationEvents.length; j < lenJ; j++) {
let activationEvent = extensionDescription.activationEvents[j];
this._activationMap[activationEvent] = this._activationMap[activationEvent] || [];
this._activationMap[activationEvent].push(extensionDescription);
}
}
}
}
public containsActivationEvent(activationEvent: string): boolean {
return hasOwnProperty.call(this._activationMap, activationEvent);
}
public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {
if (!hasOwnProperty.call(this._activationMap, activationEvent)) {
return [];
}
return this._activationMap[activationEvent].slice(0);
}
public getAllExtensionDescriptions(): IExtensionDescription[] {
return this._extensionsArr.slice(0);
}
public getExtensionDescription(extensionId: string): IExtensionDescription {
if (!hasOwnProperty.call(this._extensionsMap, extensionId)) {
return null;
}
return this._extensionsMap[extensionId];
}
}

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base';
export class LazyPromise {
private _onCancel: () => void;
private _actual: TPromise<any>;
private _actualOk: ValueCallback;
private _actualErr: ErrorCallback;
private _hasValue: boolean;
private _value: any;
private _hasErr: boolean;
private _err: any;
private _isCanceled: boolean;
constructor(onCancel: () => void) {
this._onCancel = onCancel;
this._actual = null;
this._actualOk = null;
this._actualErr = null;
this._hasValue = false;
this._value = null;
this._hasErr = false;
this._err = null;
this._isCanceled = false;
}
private _ensureActual(): TPromise<any> {
if (!this._actual) {
this._actual = new TPromise<any>((c, e) => {
this._actualOk = c;
this._actualErr = e;
}, this._onCancel);
if (this._hasValue) {
this._actualOk(this._value);
}
if (this._hasErr) {
this._actualErr(this._err);
}
}
return this._actual;
}
public resolveOk(value: any): void {
if (this._isCanceled || this._hasErr) {
return;
}
this._hasValue = true;
this._value = value;
if (this._actual) {
this._actualOk(value);
}
}
public resolveErr(err: any): void {
if (this._isCanceled || this._hasValue) {
return;
}
this._hasErr = true;
this._err = err;
if (this._actual) {
this._actualErr(err);
}
}
public then(success: any, error: any): any {
if (this._isCanceled) {
return;
}
return this._ensureActual().then(success, error);
}
public done(success: any, error: any): void {
if (this._isCanceled) {
return;
}
this._ensureActual().done(success, error);
}
public cancel(): void {
if (this._hasValue || this._hasErr) {
return;
}
this._isCanceled = true;
if (this._actual) {
this._actual.cancel();
} else {
this._onCancel();
}
}
}

View File

@@ -0,0 +1,198 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import * as marshalling from 'vs/base/common/marshalling';
import * as errors from 'vs/base/common/errors';
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise';
export interface IDispatcher {
invoke(proxyId: string, methodName: string, args: any[]): any;
}
export class RPCProtocol {
private _isDisposed: boolean;
private _bigHandler: IDispatcher;
private _lastMessageId: number;
private readonly _invokedHandlers: { [req: string]: TPromise<any>; };
private readonly _pendingRPCReplies: { [msgId: string]: LazyPromise; };
private readonly _multiplexor: RPCMultiplexer;
constructor(protocol: IMessagePassingProtocol) {
this._isDisposed = false;
this._bigHandler = null;
this._lastMessageId = 0;
this._invokedHandlers = Object.create(null);
this._pendingRPCReplies = {};
this._multiplexor = new RPCMultiplexer(protocol, (msg) => this._receiveOneMessage(msg));
}
public dispose(): void {
this._isDisposed = true;
// Release all outstanding promises with a canceled error
Object.keys(this._pendingRPCReplies).forEach((msgId) => {
const pending = this._pendingRPCReplies[msgId];
pending.resolveErr(errors.canceled());
});
}
private _receiveOneMessage(rawmsg: string): void {
if (this._isDisposed) {
console.warn('Received message after being shutdown: ', rawmsg);
return;
}
let msg = marshalling.parse(rawmsg);
if (msg.seq) {
if (!this._pendingRPCReplies.hasOwnProperty(msg.seq)) {
console.warn('Got reply to unknown seq');
return;
}
let reply = this._pendingRPCReplies[msg.seq];
delete this._pendingRPCReplies[msg.seq];
if (msg.err) {
let err = msg.err;
if (msg.err.$isError) {
err = new Error();
err.name = msg.err.name;
err.message = msg.err.message;
err.stack = msg.err.stack;
}
reply.resolveErr(err);
return;
}
reply.resolveOk(msg.res);
return;
}
if (msg.cancel) {
if (this._invokedHandlers[msg.cancel]) {
this._invokedHandlers[msg.cancel].cancel();
}
return;
}
if (msg.err) {
console.error(msg.err);
return;
}
let rpcId = msg.rpcId;
if (!this._bigHandler) {
throw new Error('got message before big handler attached!');
}
let req = msg.req;
this._invokedHandlers[req] = this._invokeHandler(rpcId, msg.method, msg.args);
this._invokedHandlers[req].then((r) => {
delete this._invokedHandlers[req];
this._multiplexor.send(MessageFactory.replyOK(req, r));
}, (err) => {
delete this._invokedHandlers[req];
this._multiplexor.send(MessageFactory.replyErr(req, err));
});
}
private _invokeHandler(proxyId: string, methodName: string, args: any[]): TPromise<any> {
try {
return TPromise.as(this._bigHandler.invoke(proxyId, methodName, args));
} catch (err) {
return TPromise.wrapError(err);
}
}
public callOnRemote(proxyId: string, methodName: string, args: any[]): TPromise<any> {
if (this._isDisposed) {
return TPromise.wrapError<any>(errors.canceled());
}
let req = String(++this._lastMessageId);
let result = new LazyPromise(() => {
this._multiplexor.send(MessageFactory.cancel(req));
});
this._pendingRPCReplies[req] = result;
this._multiplexor.send(MessageFactory.request(req, proxyId, methodName, args));
return result;
}
public setDispatcher(handler: IDispatcher): void {
this._bigHandler = handler;
}
}
/**
* Sends/Receives multiple messages in one go:
* - multiple messages to be sent from one stack get sent in bulk at `process.nextTick`.
* - each incoming message is handled in a separate `process.nextTick`.
*/
class RPCMultiplexer {
private readonly _protocol: IMessagePassingProtocol;
private readonly _sendAccumulatedBound: () => void;
private _messagesToSend: string[];
constructor(protocol: IMessagePassingProtocol, onMessage: (msg: string) => void) {
this._protocol = protocol;
this._sendAccumulatedBound = this._sendAccumulated.bind(this);
this._messagesToSend = [];
this._protocol.onMessage(data => {
for (let i = 0, len = data.length; i < len; i++) {
onMessage(data[i]);
}
});
}
private _sendAccumulated(): void {
const tmp = this._messagesToSend;
this._messagesToSend = [];
this._protocol.send(tmp);
}
public send(msg: string): void {
if (this._messagesToSend.length === 0) {
process.nextTick(this._sendAccumulatedBound);
}
this._messagesToSend.push(msg);
}
}
class MessageFactory {
public static cancel(req: string): string {
return `{"cancel":"${req}"}`;
}
public static request(req: string, rpcId: string, method: string, args: any[]): string {
return `{"req":"${req}","rpcId":"${rpcId}","method":"${method}","args":${marshalling.stringify(args)}}`;
}
public static replyOK(req: string, res: any): string {
if (typeof res === 'undefined') {
return `{"seq":"${req}"}`;
}
return `{"seq":"${req}","res":${marshalling.stringify(res)}}`;
}
public static replyErr(req: string, err: any): string {
if (typeof err === 'undefined') {
return `{"seq":"${req}","err":null}`;
}
return `{"seq":"${req}","err":${marshalling.stringify(errors.transformErrorForSerialization(err))}}`;
}
}

View File

@@ -0,0 +1,308 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import paths = require('vs/base/common/paths');
import encoding = require('vs/base/node/encoding');
import errors = require('vs/base/common/errors');
import uri from 'vs/base/common/uri';
import { toResource } from 'vs/workbench/common/editor';
import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent } from 'vs/platform/files/common/files';
import { FileService as NodeFileService, IFileServiceOptions, IEncodingOverride } from 'vs/workbench/services/files/node/fileService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { Action } from 'vs/base/common/actions';
import { ResourceMap } from 'vs/base/common/map';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IMessageService, IMessageWithAction, Severity, CloseAction } from 'vs/platform/message/common/message';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import Event, { Emitter } from 'vs/base/common/event';
import { shell } from 'electron';
export class FileService implements IFileService {
public _serviceBrand: any;
// If we run with .NET framework < 4.5, we need to detect this error to inform the user
private static NET_VERSION_ERROR = 'System.MissingMethodException';
private static NET_VERSION_ERROR_IGNORE_KEY = 'ignoreNetVersionError';
private raw: IFileService;
private toUnbind: IDisposable[];
private activeOutOfWorkspaceWatchers: ResourceMap<uri>;
protected _onFileChanges: Emitter<FileChangesEvent>;
private _onAfterOperation: Emitter<FileOperationEvent>;
constructor(
@IConfigurationService private configurationService: IConfigurationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IEditorGroupService private editorGroupService: IEditorGroupService,
@ILifecycleService private lifecycleService: ILifecycleService,
@IMessageService private messageService: IMessageService,
@IStorageService private storageService: IStorageService
) {
this.toUnbind = [];
this.activeOutOfWorkspaceWatchers = new ResourceMap<uri>();
this._onFileChanges = new Emitter<FileChangesEvent>();
this.toUnbind.push(this._onFileChanges);
this._onAfterOperation = new Emitter<FileOperationEvent>();
this.toUnbind.push(this._onAfterOperation);
const configuration = this.configurationService.getConfiguration<IFilesConfiguration>();
let watcherIgnoredPatterns: string[] = [];
if (configuration.files && configuration.files.watcherExclude) {
watcherIgnoredPatterns = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]);
}
// build config
const fileServiceConfig: IFileServiceOptions = {
errorLogger: (msg: string) => this.onFileServiceError(msg),
encodingOverride: this.getEncodingOverrides(),
watcherIgnoredPatterns,
verboseLogging: environmentService.verbose,
useExperimentalFileWatcher: configuration.files.useExperimentalFileWatcher
};
// create service
this.raw = new NodeFileService(contextService, configurationService, fileServiceConfig);
// Listeners
this.registerListeners();
}
public get onFileChanges(): Event<FileChangesEvent> {
return this._onFileChanges.event;
}
public get onAfterOperation(): Event<FileOperationEvent> {
return this._onAfterOperation.event;
}
private onFileServiceError(msg: string): void {
errors.onUnexpectedError(msg);
// Detect if we run < .NET Framework 4.5
if (typeof msg === 'string' && msg.indexOf(FileService.NET_VERSION_ERROR) >= 0 && !this.storageService.getBoolean(FileService.NET_VERSION_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) {
this.messageService.show(Severity.Warning, <IMessageWithAction>{
message: nls.localize('netVersionError', "The Microsoft .NET Framework 4.5 is required. Please follow the link to install it."),
actions: [
new Action('install.net', nls.localize('installNet', "Download .NET Framework 4.5"), null, true, () => {
window.open('https://go.microsoft.com/fwlink/?LinkId=786533');
return TPromise.as(true);
}),
new Action('net.error.ignore', nls.localize('neverShowAgain', "Don't Show Again"), '', true, () => {
this.storageService.store(FileService.NET_VERSION_ERROR_IGNORE_KEY, true, StorageScope.WORKSPACE);
return TPromise.as(null);
}),
CloseAction
]
});
}
}
private registerListeners(): void {
// File events
this.toUnbind.push(this.raw.onFileChanges(e => this._onFileChanges.fire(e)));
this.toUnbind.push(this.raw.onAfterOperation(e => this._onAfterOperation.fire(e)));
// Config changes
this.toUnbind.push(this.configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(this.configurationService.getConfiguration<IFilesConfiguration>())));
// Editor changing
this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
// Root changes
this.toUnbind.push(this.contextService.onDidChangeWorkspaceRoots(() => this.onDidChangeWorkspaceRoots()));
// Lifecycle
this.lifecycleService.onShutdown(this.dispose, this);
}
private onDidChangeWorkspaceRoots(): void {
this.updateOptions({ encodingOverride: this.getEncodingOverrides() });
}
private getEncodingOverrides(): IEncodingOverride[] {
const encodingOverride: IEncodingOverride[] = [];
encodingOverride.push({ resource: uri.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 });
if (this.contextService.hasWorkspace()) {
this.contextService.getWorkspace().roots.forEach(root => {
// {{SQL CARBON EDIT}}
encodingOverride.push({ resource: uri.file(paths.join(root.fsPath, '.sqlops')), encoding: encoding.UTF8 });
});
}
return encodingOverride;
}
private onEditorsChanged(): void {
this.handleOutOfWorkspaceWatchers();
}
private handleOutOfWorkspaceWatchers(): void {
const visibleOutOfWorkspacePaths = new ResourceMap<uri>();
this.editorService.getVisibleEditors().map(editor => {
return toResource(editor.input, { supportSideBySide: true, filter: 'file' });
}).filter(fileResource => {
return !!fileResource && !this.contextService.isInsideWorkspace(fileResource);
}).forEach(resource => {
visibleOutOfWorkspacePaths.set(resource, resource);
});
// Handle no longer visible out of workspace resources
this.activeOutOfWorkspaceWatchers.forEach(resource => {
if (!visibleOutOfWorkspacePaths.get(resource)) {
this.unwatchFileChanges(resource);
this.activeOutOfWorkspaceWatchers.delete(resource);
}
});
// Handle newly visible out of workspace resources
visibleOutOfWorkspacePaths.forEach(resource => {
if (!this.activeOutOfWorkspaceWatchers.get(resource)) {
this.watchFileChanges(resource);
this.activeOutOfWorkspaceWatchers.set(resource, resource);
}
});
}
private onConfigurationChange(configuration: IFilesConfiguration): void {
this.updateOptions(configuration.files);
}
public updateOptions(options: object): void {
this.raw.updateOptions(options);
}
public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise<IFileStat> {
return this.raw.resolveFile(resource, options);
}
public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise<IResolveFileResult[]> {
return this.raw.resolveFiles(toResolve);
}
public existsFile(resource: uri): TPromise<boolean> {
return this.raw.existsFile(resource);
}
public resolveContent(resource: uri, options?: IResolveContentOptions): TPromise<IContent> {
return this.raw.resolveContent(resource, options);
}
public resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise<IStreamContent> {
return this.raw.resolveStreamContent(resource, options);
}
public updateContent(resource: uri, value: string, options?: IUpdateContentOptions): TPromise<IFileStat> {
return this.raw.updateContent(resource, value, options);
}
public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise<IFileStat> {
return this.raw.moveFile(source, target, overwrite);
}
public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise<IFileStat> {
return this.raw.copyFile(source, target, overwrite);
}
public createFile(resource: uri, content?: string): TPromise<IFileStat> {
return this.raw.createFile(resource, content);
}
public createFolder(resource: uri): TPromise<IFileStat> {
return this.raw.createFolder(resource);
}
public touchFile(resource: uri): TPromise<IFileStat> {
return this.raw.touchFile(resource);
}
public rename(resource: uri, newName: string): TPromise<IFileStat> {
return this.raw.rename(resource, newName);
}
public del(resource: uri, useTrash?: boolean): TPromise<void> {
if (useTrash) {
return this.doMoveItemToTrash(resource);
}
return this.raw.del(resource);
}
private doMoveItemToTrash(resource: uri): TPromise<void> {
const absolutePath = resource.fsPath;
const result = shell.moveItemToTrash(absolutePath);
if (!result) {
return TPromise.wrapError<void>(new Error(nls.localize('trashFailed', "Failed to move '{0}' to the trash", paths.basename(absolutePath))));
}
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
return TPromise.as(null);
}
public importFile(source: uri, targetFolder: uri): TPromise<IImportResult> {
return this.raw.importFile(source, targetFolder).then((result) => {
return <IImportResult>{
isNew: result && result.isNew,
stat: result && result.stat
};
});
}
public watchFileChanges(resource: uri): void {
if (!resource) {
return;
}
if (resource.scheme !== 'file') {
return; // only support files
}
// return early if the resource is inside the workspace for which we have another watcher in place
if (this.contextService.isInsideWorkspace(resource)) {
return;
}
this.raw.watchFileChanges(resource);
}
public unwatchFileChanges(resource: uri): void {
this.raw.unwatchFileChanges(resource);
}
public getEncoding(resource: uri, preferredEncoding?: string): string {
return this.raw.getEncoding(resource, preferredEncoding);
}
public dispose(): void {
this.toUnbind = dispose(this.toUnbind);
// Dispose watchers if any
this.activeOutOfWorkspaceWatchers.forEach(resource => this.unwatchFileChanges(resource));
this.activeOutOfWorkspaceWatchers.clear();
// Dispose service
this.raw.dispose();
}
}

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import URI from 'vs/base/common/uri';
import { FileService } from 'vs/workbench/services/files/electron-browser/fileService';
import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, FileChangeType } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
import Event from 'vs/base/common/event';
import { EventEmitter } from 'events';
import { basename } from 'path';
import { IDisposable } from 'vs/base/common/lifecycle';
export interface IRemoteFileSystemProvider {
onDidChange: Event<URI>;
resolve(resource: URI): TPromise<string>;
update(resource: URI, content: string): TPromise<any>;
}
export class RemoteFileService extends FileService {
private readonly _provider = new Map<string, IRemoteFileSystemProvider>();
registerProvider(authority: string, provider: IRemoteFileSystemProvider): IDisposable {
if (this._provider.has(authority)) {
throw new Error();
}
this._provider.set(authority, provider);
const reg = provider.onDidChange(e => {
// forward change events
this._onFileChanges.fire(new FileChangesEvent([{ resource: e, type: FileChangeType.UPDATED }]));
});
return {
dispose: () => {
this._provider.delete(authority);
reg.dispose();
}
};
}
// --- resolve
resolveContent(resource: URI, options?: IResolveContentOptions): TPromise<IContent> {
if (this._provider.has(resource.authority)) {
return this._doResolveContent(resource);
}
return super.resolveContent(resource, options);
}
resolveStreamContent(resource: URI, options?: IResolveContentOptions): TPromise<IStreamContent> {
if (this._provider.has(resource.authority)) {
return this._doResolveContent(resource).then(RemoteFileService._asStreamContent);
}
return super.resolveStreamContent(resource, options);
}
private async _doResolveContent(resource: URI): TPromise<IContent> {
const stat = RemoteFileService._createFakeStat(resource);
const value = await this._provider.get(resource.authority).resolve(resource);
return <any>{ ...stat, value };
}
// --- saving
updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise<IFileStat> {
if (this._provider.has(resource.authority)) {
return this._doUpdateContent(resource, value).then(RemoteFileService._createFakeStat);
}
return super.updateContent(resource, value, options);
}
private async _doUpdateContent(resource: URI, content: string): TPromise<URI> {
await this._provider.get(resource.authority).update(resource, content);
return resource;
}
// --- util
private static _createFakeStat(resource: URI): IFileStat {
return <IFileStat>{
resource,
name: basename(resource.path),
encoding: 'utf8',
mtime: Date.now(),
etag: Date.now().toString(16),
isDirectory: false,
hasChildren: false
};
}
private static _asStreamContent(content: IContent): IStreamContent {
const emitter = new EventEmitter();
const { value } = content;
const result = <IStreamContent><any>content;
result.value = emitter;
setTimeout(() => {
emitter.emit('data', value);
emitter.emit('end');
}, 0);
return result;
}
}

View File

@@ -0,0 +1,981 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import paths = require('path');
import fs = require('fs');
import os = require('os');
import crypto = require('crypto');
import assert = require('assert');
import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, MAX_FILE_SIZE, FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files';
import { isEqualOrParent } from 'vs/base/common/paths';
import { ResourceMap } from 'vs/base/common/map';
import arrays = require('vs/base/common/arrays');
import baseMime = require('vs/base/common/mime');
import { TPromise } from 'vs/base/common/winjs.base';
import types = require('vs/base/common/types');
import objects = require('vs/base/common/objects');
import extfs = require('vs/base/node/extfs');
import { nfcall, ThrottledDelayer } from 'vs/base/common/async';
import uri from 'vs/base/common/uri';
import nls = require('vs/nls');
import { isWindows, isLinux } from 'vs/base/common/platform';
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import pfs = require('vs/base/node/pfs');
import encoding = require('vs/base/node/encoding');
import { IMimeAndEncoding, detectMimesFromFile } from 'vs/base/node/mime';
import flow = require('vs/base/node/flow');
import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService';
import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService';
import { toFileChangesEvent, normalize, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import Event, { Emitter } from 'vs/base/common/event';
import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export interface IEncodingOverride {
resource: uri;
encoding: string;
}
export interface IFileServiceOptions {
tmpDir?: string;
errorLogger?: (msg: string) => void;
encodingOverride?: IEncodingOverride[];
watcherIgnoredPatterns?: string[];
disableWatcher?: boolean;
verboseLogging?: boolean;
useExperimentalFileWatcher?: boolean;
}
function etag(stat: fs.Stats): string;
function etag(size: number, mtime: number): string;
function etag(arg1: any, arg2?: any): string {
let size: number;
let mtime: number;
if (typeof arg2 === 'number') {
size = arg1;
mtime = arg2;
} else {
size = (<fs.Stats>arg1).size;
mtime = (<fs.Stats>arg1).mtime.getTime();
}
return `"${crypto.createHash('sha1').update(String(size) + String(mtime)).digest('hex')}"`;
}
export class FileService implements IFileService {
public _serviceBrand: any;
private static FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private static FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms)
private tmpPath: string;
private options: IFileServiceOptions;
private _onFileChanges: Emitter<FileChangesEvent>;
private _onAfterOperation: Emitter<FileOperationEvent>;
private toDispose: IDisposable[];
private activeFileChangesWatchers: ResourceMap<fs.FSWatcher>;
private fileChangesWatchDelayer: ThrottledDelayer<void>;
private undeliveredRawFileChangesEvents: IRawFileChange[];
private activeWorkspaceChangeWatcher: IDisposable;
private currentWorkspaceRootsCount: number;
constructor(
private contextService: IWorkspaceContextService,
private configurationService: IConfigurationService,
options: IFileServiceOptions,
) {
this.toDispose = [];
this.options = options || Object.create(null);
this.tmpPath = this.options.tmpDir || os.tmpdir();
this.currentWorkspaceRootsCount = contextService.hasWorkspace() ? contextService.getWorkspace().roots.length : 0;
this._onFileChanges = new Emitter<FileChangesEvent>();
this.toDispose.push(this._onFileChanges);
this._onAfterOperation = new Emitter<FileOperationEvent>();
this.toDispose.push(this._onAfterOperation);
if (!this.options.errorLogger) {
this.options.errorLogger = console.error;
}
if (this.currentWorkspaceRootsCount > 0 && !this.options.disableWatcher) {
this.setupWorkspaceWatching();
}
this.activeFileChangesWatchers = new ResourceMap<fs.FSWatcher>();
this.fileChangesWatchDelayer = new ThrottledDelayer<void>(FileService.FS_EVENT_DELAY);
this.undeliveredRawFileChangesEvents = [];
this.registerListeners();
}
private registerListeners(): void {
this.toDispose.push(this.contextService.onDidChangeWorkspaceRoots(() => this.onDidChangeWorkspaceRoots()));
}
private onDidChangeWorkspaceRoots(): void {
const newRootCount = this.contextService.hasWorkspace() ? this.contextService.getWorkspace().roots.length : 0;
let restartWorkspaceWatcher = false;
if (this.currentWorkspaceRootsCount <= 1 && newRootCount > 1) {
restartWorkspaceWatcher = true; // transition: from 1 or 0 folders to 2+
} else if (this.currentWorkspaceRootsCount > 1 && newRootCount <= 1) {
restartWorkspaceWatcher = true; // transition: from 2+ folders to 1 or 0
}
if (restartWorkspaceWatcher) {
this.setupWorkspaceWatching();
}
this.currentWorkspaceRootsCount = newRootCount;
}
public get onFileChanges(): Event<FileChangesEvent> {
return this._onFileChanges.event;
}
public get onAfterOperation(): Event<FileOperationEvent> {
return this._onAfterOperation.event;
}
public updateOptions(options: IFileServiceOptions): void {
if (options) {
objects.mixin(this.options, options); // overwrite current options
}
}
private setupWorkspaceWatching(): void {
// dispose old if any
if (this.activeWorkspaceChangeWatcher) {
this.activeWorkspaceChangeWatcher.dispose();
}
// new watcher: use it if setting tells us so or we run in multi-root environment
if (this.options.useExperimentalFileWatcher || this.contextService.getWorkspace().roots.length > 1) {
this.activeWorkspaceChangeWatcher = toDisposable(this.setupNsfwWorkspaceWatching().startWatching());
}
// old watcher
else {
if (isWindows) {
this.activeWorkspaceChangeWatcher = toDisposable(this.setupWin32WorkspaceWatching().startWatching());
} else {
this.activeWorkspaceChangeWatcher = toDisposable(this.setupUnixWorkspaceWatching().startWatching());
}
}
}
private setupWin32WorkspaceWatching(): WindowsWatcherService {
return new WindowsWatcherService(this.contextService, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging);
}
private setupUnixWorkspaceWatching(): UnixWatcherService {
return new UnixWatcherService(this.contextService, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging);
}
private setupNsfwWorkspaceWatching(): NsfwWatcherService {
return new NsfwWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging);
}
public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise<IFileStat> {
return this.resolve(resource, options);
}
public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise<IResolveFileResult[]> {
return TPromise.join(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options)
.then(stat => ({ stat, success: true }), error => ({ stat: undefined, success: false }))));
}
public existsFile(resource: uri): TPromise<boolean> {
return this.resolveFile(resource).then(() => true, () => false);
}
public resolveContent(resource: uri, options?: IResolveContentOptions): TPromise<IContent> {
return this.doResolveContent(resource, options, (stat, enc) => this.resolveFileContent(stat, enc));
}
public resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise<IStreamContent> {
return this.doResolveContent(resource, options, (stat, enc) => this.resolveFileStreamContent(stat, enc));
}
private doResolveContent<IStreamContent>(resource: uri, options: IResolveContentOptions, contentResolver: (stat: IFileStat, enc?: string) => TPromise<IStreamContent>): TPromise<IStreamContent> {
const absolutePath = this.toAbsolutePath(resource);
// Guard early against attempts to resolve an invalid file path
if (resource.scheme !== 'file' || !resource.fsPath) {
return TPromise.wrapError<IStreamContent>(new FileOperationError(
nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString()),
FileOperationResult.FILE_INVALID_PATH
));
}
// 1.) resolve resource
return this.resolve(resource).then((model): TPromise<IStreamContent> => {
// Return early if resource is a directory
if (model.isDirectory) {
return TPromise.wrapError<IStreamContent>(new FileOperationError(
nls.localize('fileIsDirectoryError', "File is directory ({0})", absolutePath),
FileOperationResult.FILE_IS_DIRECTORY
));
}
// Return early if file not modified since
if (options && options.etag && options.etag === model.etag) {
return TPromise.wrapError<IStreamContent>(new FileOperationError(nls.localize('fileNotModifiedError', "File not modified since"), FileOperationResult.FILE_NOT_MODIFIED_SINCE));
}
// Return early if file is too large to load
if (types.isNumber(model.size) && model.size > MAX_FILE_SIZE) {
return TPromise.wrapError<IStreamContent>(new FileOperationError(nls.localize('fileTooLargeError', "File too large to open"), FileOperationResult.FILE_TOO_LARGE));
}
// 2.) detect mimes
const autoGuessEncoding = (options && options.autoGuessEncoding) || this.configuredAutoGuessEncoding(resource);
return detectMimesFromFile(absolutePath, { autoGuessEncoding }).then((detected: IMimeAndEncoding) => {
const isText = detected.mimes.indexOf(baseMime.MIME_BINARY) === -1;
// Return error early if client only accepts text and this is not text
if (options && options.acceptTextOnly && !isText) {
return TPromise.wrapError<IStreamContent>(new FileOperationError(
nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"),
FileOperationResult.FILE_IS_BINARY
));
}
let preferredEncoding: string;
if (options && options.encoding) {
if (detected.encoding === encoding.UTF8 && options.encoding === encoding.UTF8) {
preferredEncoding = encoding.UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8
} else {
preferredEncoding = options.encoding; // give passed in encoding highest priority
}
} else if (detected.encoding) {
if (detected.encoding === encoding.UTF8) {
preferredEncoding = encoding.UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM
} else {
preferredEncoding = detected.encoding;
}
} else if (this.configuredEncoding(resource) === encoding.UTF8_with_bom) {
preferredEncoding = encoding.UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then
}
// 3.) get content
return contentResolver(model, preferredEncoding);
});
}, (error) => {
// bubble up existing file operation results
if (!types.isUndefinedOrNull((<FileOperationError>error).fileOperationResult)) {
return TPromise.wrapError<IStreamContent>(error);
}
// check if the file does not exist
return pfs.exists(absolutePath).then(exists => {
// Return if file not found
if (!exists) {
return TPromise.wrapError<IStreamContent>(new FileOperationError(
nls.localize('fileNotFoundError', "File not found ({0})", absolutePath),
FileOperationResult.FILE_NOT_FOUND
));
}
// otherwise just give up
return TPromise.wrapError<IStreamContent>(error);
});
});
}
public updateContent(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise<IFileStat> {
const absolutePath = this.toAbsolutePath(resource);
// 1.) check file
return this.checkFile(absolutePath, options).then(exists => {
let createParentsPromise: TPromise<boolean>;
if (exists) {
createParentsPromise = TPromise.as(null);
} else {
createParentsPromise = pfs.mkdirp(paths.dirname(absolutePath));
}
// 2.) create parents as needed
return createParentsPromise.then(() => {
const encodingToWrite = this.getEncoding(resource, options.encoding);
let addBomPromise: TPromise<boolean> = TPromise.as(false);
// UTF_16 BE and LE as well as UTF_8 with BOM always have a BOM
if (encodingToWrite === encoding.UTF16be || encodingToWrite === encoding.UTF16le || encodingToWrite === encoding.UTF8_with_bom) {
addBomPromise = TPromise.as(true);
}
// Existing UTF-8 file: check for options regarding BOM
else if (exists && encodingToWrite === encoding.UTF8) {
if (options.overwriteEncoding) {
addBomPromise = TPromise.as(false); // if we are to overwrite the encoding, we do not preserve it if found
} else {
addBomPromise = encoding.detectEncodingByBOM(absolutePath).then(enc => enc === encoding.UTF8); // otherwise preserve it if found
}
}
// 3.) check to add UTF BOM
return addBomPromise.then(addBom => {
// 4.) set contents and resolve
return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { mode: 0o666, flag: 'w' }).then(undefined, error => {
if (!exists || error.code !== 'EPERM' || !isWindows) {
return TPromise.wrapError(error);
}
// On Windows and if the file exists with an EPERM error, we try a different strategy of saving the file
// by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows
// (see https://github.com/Microsoft/vscode/issues/931)
// 5.) truncate
return pfs.truncate(absolutePath, 0).then(() => {
// 6.) set contents (this time with r+ mode) and resolve again
return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { mode: 0o666, flag: 'r+' });
});
});
});
});
});
}
private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string, addBOM: boolean, encodingToWrite: string, options: { mode?: number; flag?: string; }): TPromise<IFileStat> {
let writeFilePromise: TPromise<void>;
// Write fast if we do UTF 8 without BOM
if (!addBOM && encodingToWrite === encoding.UTF8) {
writeFilePromise = pfs.writeFile(absolutePath, value, options);
}
// Otherwise use encoding lib
else {
const encoded = encoding.encode(value, encodingToWrite, { addBOM });
writeFilePromise = pfs.writeFile(absolutePath, encoded, options);
}
// set contents
return writeFilePromise.then(() => {
// resolve
return this.resolve(resource);
});
}
public createFile(resource: uri, content: string = ''): TPromise<IFileStat> {
// Create file
return this.updateContent(resource, content).then(result => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result));
return result;
});
}
public createFolder(resource: uri): TPromise<IFileStat> {
// 1.) Create folder
const absolutePath = this.toAbsolutePath(resource);
return pfs.mkdirp(absolutePath).then(() => {
// 2.) Resolve
return this.resolve(resource).then(result => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result));
return result;
});
});
}
public touchFile(resource: uri): TPromise<IFileStat> {
const absolutePath = this.toAbsolutePath(resource);
// 1.) check file
return this.checkFile(absolutePath).then(exists => {
let createPromise: TPromise<IFileStat>;
if (exists) {
createPromise = TPromise.as(null);
} else {
createPromise = this.createFile(resource);
}
// 2.) create file as needed
return createPromise.then(() => {
// 3.) update atime and mtime
return pfs.touch(absolutePath).then(() => {
// 4.) resolve
return this.resolve(resource);
});
});
});
}
public rename(resource: uri, newName: string): TPromise<IFileStat> {
const newPath = paths.join(paths.dirname(resource.fsPath), newName);
return this.moveFile(resource, uri.file(newPath));
}
public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise<IFileStat> {
return this.moveOrCopyFile(source, target, false, overwrite);
}
public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise<IFileStat> {
return this.moveOrCopyFile(source, target, true, overwrite);
}
private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): TPromise<IFileStat> {
const sourcePath = this.toAbsolutePath(source);
const targetPath = this.toAbsolutePath(target);
// 1.) move / copy
return this.doMoveOrCopyFile(sourcePath, targetPath, keepCopy, overwrite).then(() => {
// 2.) resolve
return this.resolve(target).then(result => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result));
return result;
});
});
}
private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): TPromise<boolean /* exists */> {
// 1.) check if target exists
return pfs.exists(targetPath).then(exists => {
const isCaseRename = sourcePath.toLowerCase() === targetPath.toLowerCase();
const isSameFile = sourcePath === targetPath;
// Return early with conflict if target exists and we are not told to overwrite
if (exists && !isCaseRename && !overwrite) {
return TPromise.wrapError<boolean>(new FileOperationError(nls.localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT));
}
// 2.) make sure target is deleted before we move/copy unless this is a case rename of the same file
let deleteTargetPromise = TPromise.as<void>(void 0);
if (exists && !isCaseRename) {
if (isEqualOrParent(sourcePath, targetPath, !isLinux /* ignorecase */)) {
return TPromise.wrapError<boolean>(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case!
}
deleteTargetPromise = this.del(uri.file(targetPath));
}
return deleteTargetPromise.then(() => {
// 3.) make sure parents exists
return pfs.mkdirp(paths.dirname(targetPath)).then(() => {
// 4.) copy/move
if (isSameFile) {
return TPromise.as(null);
} else if (keepCopy) {
return nfcall(extfs.copy, sourcePath, targetPath);
} else {
return nfcall(extfs.mv, sourcePath, targetPath);
}
}).then(() => exists);
});
});
}
public importFile(source: uri, targetFolder: uri): TPromise<IImportResult> {
const sourcePath = this.toAbsolutePath(source);
const targetResource = uri.file(paths.join(targetFolder.fsPath, paths.basename(source.fsPath)));
const targetPath = this.toAbsolutePath(targetResource);
// 1.) resolve
return pfs.stat(sourcePath).then(stat => {
if (stat.isDirectory()) {
return TPromise.wrapError<IImportResult>(new Error(nls.localize('foldersCopyError', "Folders cannot be copied into the workspace. Please select individual files to copy them."))); // for now we do not allow to import a folder into a workspace
}
// 2.) copy
return this.doMoveOrCopyFile(sourcePath, targetPath, true, true).then(exists => {
// 3.) resolve
return this.resolve(targetResource).then(stat => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.IMPORT, stat));
return <IImportResult>{ isNew: !exists, stat: stat };
});
});
});
}
public del(resource: uri): TPromise<void> {
const absolutePath = this.toAbsolutePath(resource);
return pfs.del(absolutePath, this.tmpPath).then(() => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
});
}
// Helpers
private toAbsolutePath(arg1: uri | IFileStat): string {
let resource: uri;
if (arg1 instanceof uri) {
resource = <uri>arg1;
} else {
resource = (<IFileStat>arg1).resource;
}
assert.ok(resource && resource.scheme === 'file', 'Invalid resource: ' + resource);
return paths.normalize(resource.fsPath);
}
private resolve(resource: uri, options: IResolveFileOptions = Object.create(null)): TPromise<IFileStat> {
return this.toStatResolver(resource)
.then(model => model.resolve(options));
}
private toStatResolver(resource: uri): TPromise<StatResolver> {
const absolutePath = this.toAbsolutePath(resource);
return pfs.stat(absolutePath).then(stat => {
return new StatResolver(resource, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.options.verboseLogging);
});
}
private resolveFileStreamContent(model: IFileStat, enc?: string): TPromise<IStreamContent> {
// Return early if file is too large to load
if (types.isNumber(model.size) && model.size > MAX_FILE_SIZE) {
return TPromise.wrapError<IStreamContent>(new FileOperationError(nls.localize('fileTooLargeError', "File too large to open"), FileOperationResult.FILE_TOO_LARGE));
}
const absolutePath = this.toAbsolutePath(model);
const fileEncoding = this.getEncoding(model.resource, enc);
const reader = fs.createReadStream(absolutePath).pipe(encoding.decodeStream(fileEncoding)); // decode takes care of stripping any BOMs from the file content
const content = model as IFileStat & IStreamContent;
content.value = reader;
content.encoding = fileEncoding; // make sure to store the encoding in the model to restore it later when writing
return TPromise.as(content);
}
private resolveFileContent(model: IFileStat, enc?: string): TPromise<IContent> {
return this.resolveFileStreamContent(model, enc).then(streamContent => {
return new TPromise<IContent>((c, e) => {
let done = false;
const chunks: string[] = [];
streamContent.value.on('data', buf => {
chunks.push(buf);
});
streamContent.value.on('error', error => {
if (!done) {
done = true;
e(error);
}
});
streamContent.value.on('end', () => {
const content: IContent = <any>streamContent;
content.value = chunks.join('');
if (!done) {
done = true;
c(content);
}
});
});
});
}
public getEncoding(resource: uri, preferredEncoding?: string): string {
let fileEncoding: string;
const override = this.getEncodingOverride(resource);
if (override) {
fileEncoding = override;
} else if (preferredEncoding) {
fileEncoding = preferredEncoding;
} else {
fileEncoding = this.configuredEncoding(resource);
}
if (!fileEncoding || !encoding.encodingExists(fileEncoding)) {
fileEncoding = encoding.UTF8; // the default is UTF 8
}
return fileEncoding;
}
private configuredAutoGuessEncoding(resource: uri): boolean {
const config = this.configurationService.getConfiguration(void 0, { resource }) as IFilesConfiguration;
return config && config.files && config.files.autoGuessEncoding === true;
}
private configuredEncoding(resource: uri): string {
const config = this.configurationService.getConfiguration(void 0, { resource }) as IFilesConfiguration;
return config && config.files && config.files.encoding;
}
private getEncodingOverride(resource: uri): string {
if (resource && this.options.encodingOverride && this.options.encodingOverride.length) {
for (let i = 0; i < this.options.encodingOverride.length; i++) {
const override = this.options.encodingOverride[i];
// check if the resource is a child of the resource with override and use
// the provided encoding in that case
if (isParent(resource.fsPath, override.resource.fsPath, !isLinux /* ignorecase */)) {
return override.encoding;
}
}
}
return null;
}
private checkFile(absolutePath: string, options: IUpdateContentOptions = Object.create(null)): TPromise<boolean /* exists */> {
return pfs.exists(absolutePath).then(exists => {
if (exists) {
return pfs.stat(absolutePath).then(stat => {
if (stat.isDirectory()) {
return TPromise.wrapError<boolean>(new Error('Expected file is actually a directory'));
}
// Dirty write prevention
if (typeof options.mtime === 'number' && typeof options.etag === 'string' && options.mtime < stat.mtime.getTime()) {
// Find out if content length has changed
if (options.etag !== etag(stat.size, options.mtime)) {
return TPromise.wrapError<boolean>(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE));
}
}
let mode = stat.mode;
const readonly = !(mode & 128);
// Throw if file is readonly and we are not instructed to overwrite
if (readonly && !options.overwriteReadonly) {
return TPromise.wrapError<boolean>(new FileOperationError(
nls.localize('fileReadOnlyError', "File is Read Only"),
FileOperationResult.FILE_READ_ONLY
));
}
if (readonly) {
mode = mode | 128;
return pfs.chmod(absolutePath, mode).then(() => exists);
}
return TPromise.as<boolean>(exists);
});
}
return TPromise.as<boolean>(exists);
});
}
public watchFileChanges(resource: uri): void {
assert.ok(resource && resource.scheme === 'file', `Invalid resource for watching: ${resource}`);
// Create or get watcher for provided path
let watcher = this.activeFileChangesWatchers.get(resource);
if (!watcher) {
const fsPath = resource.fsPath;
try {
watcher = fs.watch(fsPath); // will be persistent but not recursive
} catch (error) {
return; // the path might not exist anymore, ignore this error and return
}
this.activeFileChangesWatchers.set(resource, watcher);
// eventType is either 'rename' or 'change'
const fsName = paths.basename(resource.fsPath);
watcher.on('change', (eventType: string, filename: string) => {
const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename');
// The file was either deleted or renamed. Many tools apply changes to files in an
// atomic way ("Atomic Save") by first renaming the file to a temporary name and then
// renaming it back to the original name. Our watcher will detect this as a rename
// and then stops to work on Mac and Linux because the watcher is applied to the
// inode and not the name. The fix is to detect this case and trying to watch the file
// again after a certain delay.
// In addition, we send out a delete event if after a timeout we detect that the file
// does indeed not exist anymore.
if (renamedOrDeleted) {
// Very important to dispose the watcher which now points to a stale inode
this.unwatchFileChanges(resource);
// Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save")
setTimeout(() => {
this.existsFile(resource).done(exists => {
// File still exists, so reapply the watcher
if (exists) {
this.watchFileChanges(resource);
}
// File seems to be really gone, so emit a deleted event
else {
this.onRawFileChange({
type: FileChangeType.DELETED,
path: fsPath
});
}
});
}, FileService.FS_REWATCH_DELAY);
}
// Handle raw file change
this.onRawFileChange({
type: FileChangeType.UPDATED,
path: fsPath
});
});
// Errors
watcher.on('error', (error: string) => {
this.options.errorLogger(error);
});
}
}
private onRawFileChange(event: IRawFileChange): void {
// add to bucket of undelivered events
this.undeliveredRawFileChangesEvents.push(event);
if (this.options.verboseLogging) {
console.log('%c[node.js Watcher]%c', 'color: green', 'color: black', event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path);
}
// handle emit through delayer to accommodate for bulk changes
this.fileChangesWatchDelayer.trigger(() => {
const buffer = this.undeliveredRawFileChangesEvents;
this.undeliveredRawFileChangesEvents = [];
// Normalize
const normalizedEvents = normalize(buffer);
// Logging
if (this.options.verboseLogging) {
normalizedEvents.forEach(r => {
console.log('%c[node.js Watcher]%c >> normalized', 'color: green', 'color: black', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path);
});
}
// Emit
this._onFileChanges.fire(toFileChangesEvent(normalizedEvents));
return TPromise.as(null);
});
}
public unwatchFileChanges(resource: uri): void {
const watcher = this.activeFileChangesWatchers.get(resource);
if (watcher) {
watcher.close();
this.activeFileChangesWatchers.delete(resource);
}
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
if (this.activeWorkspaceChangeWatcher) {
this.activeWorkspaceChangeWatcher.dispose();
this.activeWorkspaceChangeWatcher = null;
}
this.activeFileChangesWatchers.forEach(watcher => watcher.close());
this.activeFileChangesWatchers.clear();
}
}
export class StatResolver {
private resource: uri;
private isDirectory: boolean;
private mtime: number;
private name: string;
private etag: string;
private size: number;
private verboseLogging: boolean;
constructor(resource: uri, isDirectory: boolean, mtime: number, size: number, verboseLogging: boolean) {
assert.ok(resource && resource.scheme === 'file', 'Invalid resource: ' + resource);
this.resource = resource;
this.isDirectory = isDirectory;
this.mtime = mtime;
this.name = paths.basename(resource.fsPath);
this.etag = etag(size, mtime);
this.size = size;
this.verboseLogging = verboseLogging;
}
public resolve(options: IResolveFileOptions): TPromise<IFileStat> {
// General Data
const fileStat: IFileStat = {
resource: this.resource,
isDirectory: this.isDirectory,
hasChildren: undefined,
name: this.name,
etag: this.etag,
size: this.size,
mtime: this.mtime
};
// File Specific Data
if (!this.isDirectory) {
return TPromise.as(fileStat);
}
// Directory Specific Data
else {
// Convert the paths from options.resolveTo to absolute paths
let absoluteTargetPaths: string[] = null;
if (options && options.resolveTo) {
absoluteTargetPaths = [];
options.resolveTo.forEach(resource => {
absoluteTargetPaths.push(resource.fsPath);
});
}
return new TPromise<IFileStat>((c, e) => {
// Load children
this.resolveChildren(this.resource.fsPath, absoluteTargetPaths, options && options.resolveSingleChildDescendants, (children) => {
children = arrays.coalesce(children); // we don't want those null children (could be permission denied when reading a child)
fileStat.hasChildren = children && children.length > 0;
fileStat.children = children || [];
c(fileStat);
});
});
}
}
private resolveChildren(absolutePath: string, absoluteTargetPaths: string[], resolveSingleChildDescendants: boolean, callback: (children: IFileStat[]) => void): void {
extfs.readdir(absolutePath, (error: Error, files: string[]) => {
if (error) {
if (this.verboseLogging) {
console.error(error);
}
return callback(null); // return - we might not have permissions to read the folder
}
// for each file in the folder
flow.parallel(files, (file: string, clb: (error: Error, children: IFileStat) => void) => {
const fileResource = uri.file(paths.resolve(absolutePath, file));
let fileStat: fs.Stats;
const $this = this;
flow.sequence(
function onError(error: Error): void {
if ($this.verboseLogging) {
console.error(error);
}
clb(null, null); // return - we might not have permissions to read the folder or stat the file
},
function stat(): void {
fs.stat(fileResource.fsPath, this);
},
function countChildren(fsstat: fs.Stats): void {
fileStat = fsstat;
if (fileStat.isDirectory()) {
extfs.readdir(fileResource.fsPath, (error, result) => {
this(null, result ? result.length : 0);
});
} else {
this(null, 0);
}
},
function resolve(childCount: number): void {
const childStat: IFileStat = {
resource: fileResource,
isDirectory: fileStat.isDirectory(),
hasChildren: childCount > 0,
name: file,
mtime: fileStat.mtime.getTime(),
etag: etag(fileStat),
size: fileStat.size
};
// Return early for files
if (!fileStat.isDirectory()) {
return clb(null, childStat);
}
// Handle Folder
let resolveFolderChildren = false;
if (files.length === 1 && resolveSingleChildDescendants) {
resolveFolderChildren = true;
} else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => isEqualOrParent(targetPath, fileResource.fsPath, !isLinux /* ignorecase */))) {
resolveFolderChildren = true;
}
// Continue resolving children based on condition
if (resolveFolderChildren) {
$this.resolveChildren(fileResource.fsPath, absoluteTargetPaths, resolveSingleChildDescendants, children => {
children = arrays.coalesce(children); // we don't want those null children
childStat.hasChildren = children && children.length > 0;
childStat.children = children || [];
clb(null, childStat);
});
}
// Otherwise return result
else {
clb(null, childStat);
}
});
}, (errors, result) => {
callback(result);
});
});
}
}

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import uri from 'vs/base/common/uri';
import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
export interface IRawFileChange {
type: FileChangeType;
path: string;
}
export function toFileChangesEvent(changes: IRawFileChange[]): FileChangesEvent {
// map to file changes event that talks about URIs
return new FileChangesEvent(changes.map((c) => {
return {
type: c.type,
resource: uri.file(c.path)
};
}));
}
/**
* Given events that occurred, applies some rules to normalize the events
*/
export function normalize(changes: IRawFileChange[]): IRawFileChange[] {
// Build deltas
let normalizer = new EventNormalizer();
for (let i = 0; i < changes.length; i++) {
let event = changes[i];
normalizer.processEvent(event);
}
return normalizer.normalize();
}
class EventNormalizer {
private normalized: IRawFileChange[];
private mapPathToChange: { [path: string]: IRawFileChange };
constructor() {
this.normalized = [];
this.mapPathToChange = Object.create(null);
}
public processEvent(event: IRawFileChange): void {
// Event path already exists
let existingEvent = this.mapPathToChange[event.path];
if (existingEvent) {
let currentChangeType = existingEvent.type;
let newChangeType = event.type;
// ignore CREATE followed by DELETE in one go
if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.DELETED) {
delete this.mapPathToChange[event.path];
this.normalized.splice(this.normalized.indexOf(existingEvent), 1);
}
// flatten DELETE followed by CREATE into CHANGE
else if (currentChangeType === FileChangeType.DELETED && newChangeType === FileChangeType.ADDED) {
existingEvent.type = FileChangeType.UPDATED;
}
// Do nothing. Keep the created event
else if (currentChangeType === FileChangeType.ADDED && newChangeType === FileChangeType.UPDATED) {
}
// Otherwise apply change type
else {
existingEvent.type = newChangeType;
}
}
// Otherwise Store
else {
this.normalized.push(event);
this.mapPathToChange[event.path] = event;
}
}
public normalize(): IRawFileChange[] {
let addedChangeEvents: IRawFileChange[] = [];
let deletedPaths: string[] = [];
// This algorithm will remove all DELETE events up to the root folder
// that got deleted if any. This ensures that we are not producing
// DELETE events for each file inside a folder that gets deleted.
//
// 1.) split ADD/CHANGE and DELETED events
// 2.) sort short deleted paths to the top
// 3.) for each DELETE, check if there is a deleted parent and ignore the event in that case
return this.normalized.filter(e => {
if (e.type !== 2) {
addedChangeEvents.push(e);
return false; // remove ADD / CHANGE
}
return true; // keep DELETE
}).sort((e1, e2) => {
return e1.path.length - e2.path.length; // shortest path first
}).filter(e => {
if (deletedPaths.some(d => isParent(e.path, d, !isLinux /* ignorecase */))) {
return false; // DELETE is ignored if parent is deleted already
}
// otherwise mark as deleted
deletedPaths.push(e.path);
return true;
}).concat(addedChangeEvents);
}
}

View File

@@ -0,0 +1,176 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as glob from 'vs/base/common/glob';
import * as paths from 'vs/base/common/paths';
import * as path from 'path';
import * as platform from 'vs/base/common/platform';
import * as watcher from 'vs/workbench/services/files/node/watcher/common';
import * as nsfw from 'nsfw';
import { IWatcherService, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
import { TPromise, ProgressCallback, TValueCallback } from 'vs/base/common/winjs.base';
import { ThrottledDelayer } from 'vs/base/common/async';
import { FileChangeType } from 'vs/platform/files/common/files';
import { normalizeNFC } from 'vs/base/common/strings';
const nsfwActionToRawChangeType: { [key: number]: number } = [];
nsfwActionToRawChangeType[nsfw.actions.CREATED] = FileChangeType.ADDED;
nsfwActionToRawChangeType[nsfw.actions.MODIFIED] = FileChangeType.UPDATED;
nsfwActionToRawChangeType[nsfw.actions.DELETED] = FileChangeType.DELETED;
interface IWatcherObjet {
start(): any;
stop(): any;
}
interface IPathWatcher {
ready: TPromise<IWatcherObjet>;
watcher?: IWatcherObjet;
ignored: string[];
}
export class NsfwWatcherService implements IWatcherService {
private static FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private _pathWatchers: { [watchPath: string]: IPathWatcher } = {};
private _watcherPromise: TPromise<void>;
private _progressCallback: ProgressCallback;
private _verboseLogging: boolean;
public initialize(verboseLogging: boolean): TPromise<void> {
this._verboseLogging = verboseLogging;
this._watcherPromise = new TPromise<void>((c, e, p) => {
this._progressCallback = p;
});
return this._watcherPromise;
}
private _watch(request: IWatcherRequest): void {
let undeliveredFileEvents: watcher.IRawFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer(NsfwWatcherService.FS_EVENT_DELAY);
let readyPromiseCallback: TValueCallback<IWatcherObjet>;
this._pathWatchers[request.basePath] = {
ready: new TPromise<IWatcherObjet>(c => readyPromiseCallback = c),
ignored: request.ignored
};
nsfw(request.basePath, events => {
for (let i = 0; i < events.length; i++) {
const e = events[i];
// Logging
if (this._verboseLogging) {
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile) + ' -> ' + e.newFile : path.join(e.directory, e.file);
console.log(e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]', logPath);
}
// Convert nsfw event to IRawFileChange and add to queue
let absolutePath: string;
if (e.action === nsfw.actions.RENAMED) {
// Rename fires when a file's name changes within a single directory
absolutePath = path.join(e.directory, e.oldFile);
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
}
absolutePath = path.join(e.directory, e.newFile);
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
}
} else {
absolutePath = path.join(e.directory, e.file);
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
undeliveredFileEvents.push({
type: nsfwActionToRawChangeType[e.action],
path: absolutePath
});
}
}
}
// Delay and send buffer
fileEventDelayer.trigger(() => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
// Mac uses NFD unicode form on disk, but we want NFC
if (platform.isMacintosh) {
events.forEach(e => e.path = normalizeNFC(e.path));
}
// Broadcast to clients normalized
const res = watcher.normalize(events);
this._progressCallback(res);
// Logging
if (this._verboseLogging) {
res.forEach(r => {
console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path);
});
}
return TPromise.as(null);
});
}).then(watcher => {
this._pathWatchers[request.basePath].watcher = watcher;
const startPromise = watcher.start();
startPromise.then(() => readyPromiseCallback(watcher));
return startPromise;
});
}
public setRoots(roots: IWatcherRequest[]): TPromise<void> {
const promises: TPromise<void>[] = [];
const normalizedRoots = this._normalizeRoots(roots);
// Gather roots that are not currently being watched
const rootsToStartWatching = normalizedRoots.filter(r => {
return !(r.basePath in this._pathWatchers);
});
// Gather current roots that don't exist in the new roots array
const rootsToStopWatching = Object.keys(this._pathWatchers).filter(r => {
return normalizedRoots.every(normalizedRoot => normalizedRoot.basePath !== r);
});
// Logging
if (this._verboseLogging) {
console.log(`Start watching: [${rootsToStartWatching.map(r => r.basePath).join(',')}]\nStop watching: [${rootsToStopWatching.join(',')}]`);
}
// Stop watching some roots
rootsToStopWatching.forEach(root => {
this._pathWatchers[root].ready.then(watcher => watcher.stop());
delete this._pathWatchers[root];
});
// Start watching some roots
rootsToStartWatching.forEach(root => this._watch(root));
// Refresh ignored arrays in case they changed
roots.forEach(root => {
if (root.basePath in this._pathWatchers) {
this._pathWatchers[root.basePath].ignored = root.ignored;
}
});
return TPromise.join(promises).then(() => void 0);
}
/**
* Normalizes a set of root paths by removing any root paths that are
* sub-paths of other roots.
*/
protected _normalizeRoots(roots: IWatcherRequest[]): IWatcherRequest[] {
return roots.filter(r => roots.every(other => {
return !(r.basePath.length > other.basePath.length && paths.isEqualOrParent(r.basePath, other.basePath));
}));
}
private _isPathIgnored(absolutePath: string, ignored: string[]): boolean {
return ignored && ignored.some(ignore => glob.match(ignore, absolutePath));
}
}

View File

@@ -0,0 +1,54 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import assert = require('assert');
import platform = require('vs/base/common/platform');
import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService';
import { IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
class TestNsfwWatcherService extends NsfwWatcherService {
public normalizeRoots(roots: string[]): string[] {
// Work with strings as paths to simplify testing
const requests: IWatcherRequest[] = roots.map(r => {
return { basePath: r, ignored: [] };
});
return this._normalizeRoots(requests).map(r => r.basePath);
}
}
suite('NSFW Watcher Service', () => {
suite('_normalizeRoots', () => {
test('should not impacts roots that don\'t overlap', () => {
const service = new TestNsfwWatcherService();
if (platform.isWindows) {
assert.deepEqual(service.normalizeRoots(['C:\\a']), ['C:\\a']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\c\\d\\e']), ['C:\\a', 'C:\\b', 'C:\\c\\d\\e']);
} else {
assert.deepEqual(service.normalizeRoots(['/a']), ['/a']);
assert.deepEqual(service.normalizeRoots(['/a', '/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/c/d/e']), ['/a', '/b', '/c/d/e']);
}
});
test('should remove sub-folders of other roots', () => {
const service = new TestNsfwWatcherService();
if (platform.isWindows) {
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b']), ['C:\\a']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\b\\a', 'C:\\a', 'C:\\b', 'C:\\a\\b']), ['C:\\a', 'C:\\b']);
assert.deepEqual(service.normalizeRoots(['C:\\a', 'C:\\a\\b', 'C:\\a\\c\\d']), ['C:\\a']);
} else {
assert.deepEqual(service.normalizeRoots(['/a', '/a/b']), ['/a']);
assert.deepEqual(service.normalizeRoots(['/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/b/a', '/a', '/b', '/a/b']), ['/a', '/b']);
assert.deepEqual(service.normalizeRoots(['/a', '/a/b', '/a/c/d']), ['/a']);
}
});
});
});

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
export interface IWatcherRequest {
basePath: string;
ignored: string[];
}
export interface IWatcherService {
initialize(verboseLogging: boolean): TPromise<void>;
setRoots(roots: IWatcherRequest[]): TPromise<void>;
}
export interface IFileWatcher {
startWatching(): () => void;
addFolder(folder: string): void;
}

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc';
import { NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/nsfwWatcherService';
const server = new Server();
const service = new NsfwWatcherService();
const channel = new WatcherChannel(service);
server.registerChannel('watcher', channel);

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService } from './watcher';
export interface IWatcherChannel extends IChannel {
call(command: 'initialize', verboseLogging: boolean): TPromise<void>;
call(command: 'setRoots', request: IWatcherRequest[]): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
}
export class WatcherChannel implements IWatcherChannel {
constructor(private service: IWatcherService) { }
call(command: string, arg: any): TPromise<any> {
switch (command) {
case 'initialize': return this.service.initialize(arg);
case 'setRoots': return this.service.setRoots(arg);
}
return undefined;
}
}
export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IWatcherChannel) { }
initialize(verboseLogging: boolean): TPromise<void> {
return this.channel.call('initialize', verboseLogging);
}
setRoots(roots: IWatcherRequest[]): TPromise<void> {
return this.channel.call('setRoots', roots);
}
}

View File

@@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import uri from 'vs/base/common/uri';
import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/nsfw/watcherIpc';
import { FileChangesEvent, IFilesConfiguration } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
export class FileWatcher {
private static MAX_RESTARTS = 5;
private service: WatcherChannelClient;
private isDisposed: boolean;
private restartCounter: number;
private toDispose: IDisposable[];
constructor(
private contextService: IWorkspaceContextService,
private configurationService: IConfigurationService,
private onFileChanges: (changes: FileChangesEvent) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean,
) {
this.isDisposed = false;
this.restartCounter = 0;
this.toDispose = [];
}
public startWatching(): () => void {
const args = ['--type=watcherService'];
const client = new Client(
uri.parse(require.toUrl('bootstrap')).fsPath,
{
serverName: 'Watcher',
args,
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/nsfw/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: this.verboseLogging
}
}
);
this.toDispose.push(client);
// Initialize watcher
const channel = getNextTickChannel(client.getChannel<IWatcherChannel>('watcher'));
this.service = new WatcherChannelClient(channel);
this.service.initialize(this.verboseLogging).then(null, err => {
if (!this.isDisposed && !(err instanceof Error && err.name === 'Canceled' && err.message === 'Canceled')) {
return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up
}
return void 0;
}, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.errorLogger('[FileWatcher] terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}, error => {
if (!this.isDisposed) {
this.errorLogger(error);
}
});
// Start watching
this.updateRoots();
this.toDispose.push(this.contextService.onDidChangeWorkspaceRoots(() => this.updateRoots()));
this.toDispose.push(this.configurationService.onDidUpdateConfiguration(() => this.updateRoots()));
return () => this.dispose();
}
private updateRoots() {
if (this.isDisposed) {
return;
}
const roots = this.contextService.getWorkspace().roots;
this.service.setRoots(roots.map(root => {
// Fetch the root's watcherExclude setting and return it
const configuration = this.configurationService.getConfiguration<IFilesConfiguration>(undefined, {
resource: root
});
let ignored: string[] = [];
if (configuration.files && configuration.files.watcherExclude) {
ignored = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]);
}
return {
basePath: root.fsPath,
ignored
};
}));
}
private onRawFileEvents(events: IRawFileChange[]): void {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(toFileChangesEvent(events));
}
}
private dispose(): void {
this.isDisposed = true;
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -0,0 +1,143 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import chokidar = require('chokidar');
import fs = require('fs');
import gracefulFs = require('graceful-fs');
gracefulFs.gracefulify(fs);
import { TPromise } from 'vs/base/common/winjs.base';
import { FileChangeType } from 'vs/platform/files/common/files';
import { ThrottledDelayer } from 'vs/base/common/async';
import strings = require('vs/base/common/strings');
import { realcaseSync } from 'vs/base/node/extfs';
import { isMacintosh } from 'vs/base/common/platform';
import watcher = require('vs/workbench/services/files/node/watcher/common');
import { IWatcherRequest, IWatcherService } from './watcher';
export class ChokidarWatcherService implements IWatcherService {
private static FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms)
private static EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000; // warn after certain time span of event spam
private spamCheckStartTime: number;
private spamWarningLogged: boolean;
public watch(request: IWatcherRequest): TPromise<void> {
const watcherOpts: chokidar.IOptions = {
ignoreInitial: true,
ignorePermissionErrors: true,
followSymlinks: true, // this is the default of chokidar and supports file events through symlinks
ignored: request.ignored,
interval: 1000, // while not used in normal cases, if any error causes chokidar to fallback to polling, increase its intervals
binaryInterval: 1000,
disableGlobbing: true // fix https://github.com/Microsoft/vscode/issues/4586
};
// Chokidar fails when the basePath does not match case-identical to the path on disk
// so we have to find the real casing of the path and do some path massaging to fix this
// see https://github.com/paulmillr/chokidar/issues/418
const originalBasePath = request.basePath;
const realBasePath = isMacintosh ? (realcaseSync(originalBasePath) || originalBasePath) : originalBasePath;
const realBasePathLength = realBasePath.length;
const realBasePathDiffers = (originalBasePath !== realBasePath);
if (realBasePathDiffers) {
console.warn(`Watcher basePath does not match version on disk and was corrected (original: ${originalBasePath}, real: ${realBasePath})`);
}
const chokidarWatcher = chokidar.watch(realBasePath, watcherOpts);
// Detect if for some reason the native watcher library fails to load
if (isMacintosh && !chokidarWatcher.options.useFsEvents) {
console.error('Watcher is not using native fsevents library and is falling back to unefficient polling.');
}
let undeliveredFileEvents: watcher.IRawFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
return new TPromise<void>((c, e, p) => {
chokidarWatcher.on('all', (type: string, path: string) => {
if (path.indexOf(realBasePath) < 0) {
return; // we really only care about absolute paths here in our basepath context here
}
// Make sure to convert the path back to its original basePath form if the realpath is different
if (realBasePathDiffers) {
path = originalBasePath + path.substr(realBasePathLength);
}
let event: watcher.IRawFileChange = null;
// Change
if (type === 'change') {
event = { type: 0, path };
}
// Add
else if (type === 'add' || type === 'addDir') {
event = { type: 1, path };
}
// Delete
else if (type === 'unlink' || type === 'unlinkDir') {
event = { type: 2, path };
}
if (event) {
// Logging
if (request.verboseLogging) {
console.log(event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path);
}
// Check for spam
const now = Date.now();
if (undeliveredFileEvents.length === 0) {
this.spamWarningLogged = false;
this.spamCheckStartTime = now;
} else if (!this.spamWarningLogged && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) {
this.spamWarningLogged = true;
console.warn(strings.format('Watcher is busy catching up with {0} file changes in 60 seconds. Latest changed path is "{1}"', undeliveredFileEvents.length, event.path));
}
// Add to buffer
undeliveredFileEvents.push(event);
// Delay and send buffer
fileEventDelayer.trigger(() => {
const events = undeliveredFileEvents;
undeliveredFileEvents = [];
// Broadcast to clients normalized
const res = watcher.normalize(events);
p(res);
// Logging
if (request.verboseLogging) {
res.forEach(r => {
console.log(' >> normalized', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path);
});
}
return TPromise.as(null);
});
}
});
chokidarWatcher.on('error', (error: Error) => {
if (error) {
console.error(error.toString());
}
});
}, () => {
chokidarWatcher.close();
fileEventDelayer.cancel();
});
}
}

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
export interface IWatcherRequest {
basePath: string;
ignored: string[];
verboseLogging: boolean;
}
export interface IWatcherService {
watch(request: IWatcherRequest): TPromise<void>;
}

View File

@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { WatcherChannel } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc';
import { ChokidarWatcherService } from 'vs/workbench/services/files/node/watcher/unix/chokidarWatcherService';
const server = new Server();
const service = new ChokidarWatcherService();
const channel = new WatcherChannel(service);
server.registerChannel('watcher', channel);

View File

@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IWatcherRequest, IWatcherService } from './watcher';
export interface IWatcherChannel extends IChannel {
call(command: 'watch', request: IWatcherRequest): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
}
export class WatcherChannel implements IWatcherChannel {
constructor(private service: IWatcherService) { }
call(command: string, arg: any): TPromise<any> {
switch (command) {
case 'watch': return this.service.watch(arg);
}
return undefined;
}
}
export class WatcherChannelClient implements IWatcherService {
constructor(private channel: IWatcherChannel) { }
watch(request: IWatcherRequest): TPromise<void> {
return this.channel.call('watch', request);
}
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import uri from 'vs/base/common/uri';
import { toFileChangesEvent, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
import { IWatcherChannel, WatcherChannelClient } from 'vs/workbench/services/files/node/watcher/unix/watcherIpc';
import { FileChangesEvent } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { normalize } from 'path';
export class FileWatcher {
private static MAX_RESTARTS = 5;
private isDisposed: boolean;
private restartCounter: number;
constructor(
private contextService: IWorkspaceContextService,
private ignored: string[],
private onFileChanges: (changes: FileChangesEvent) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean
) {
this.isDisposed = false;
this.restartCounter = 0;
}
public startWatching(): () => void {
const args = ['--type=watcherService'];
const client = new Client(
uri.parse(require.toUrl('bootstrap')).fsPath,
{
serverName: 'Watcher',
args,
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/files/node/watcher/unix/watcherApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: this.verboseLogging
}
}
);
const channel = getNextTickChannel(client.getChannel<IWatcherChannel>('watcher'));
const service = new WatcherChannelClient(channel);
// Start watching
const basePath: string = normalize(this.contextService.getWorkspace().roots[0].fsPath);
service.watch({ basePath: basePath, ignored: this.ignored, verboseLogging: this.verboseLogging }).then(null, err => {
if (!this.isDisposed && !(err instanceof Error && err.name === 'Canceled' && err.message === 'Canceled')) {
return TPromise.wrapError(err); // the service lib uses the promise cancel error to indicate the process died, we do not want to bubble this up
}
return void 0;
}, (events: IRawFileChange[]) => this.onRawFileEvents(events)).done(() => {
// our watcher app should never be completed because it keeps on watching. being in here indicates
// that the watcher process died and we want to restart it here. we only do it a max number of times
if (!this.isDisposed) {
if (this.restartCounter <= FileWatcher.MAX_RESTARTS) {
this.errorLogger('[FileWatcher] terminated unexpectedly and is restarted again...');
this.restartCounter++;
this.startWatching();
} else {
this.errorLogger('[FileWatcher] failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}, error => {
if (!this.isDisposed) {
this.errorLogger(error);
}
});
return () => {
this.isDisposed = true;
client.dispose();
};
}
private onRawFileEvents(events: IRawFileChange[]): void {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(toFileChangesEvent(events));
}
}
}

View File

@@ -0,0 +1,8 @@
# Native File Watching for Windows using C# FileSystemWatcher
- Repository: https://github.com/Microsoft/vscode-filewatcher-windows
# Build
- Build in "Release" config
- Copy CodeHelper.exe over into this folder

View File

@@ -0,0 +1,119 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import cp = require('child_process');
import { FileChangeType } from 'vs/platform/files/common/files';
import decoder = require('vs/base/node/decoder');
import glob = require('vs/base/common/glob');
import uri from 'vs/base/common/uri';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
export class OutOfProcessWin32FolderWatcher {
private static MAX_RESTARTS = 5;
private static changeTypeMap: FileChangeType[] = [FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED];
private handle: cp.ChildProcess;
private restartCounter: number;
constructor(
private watchedFolder: string,
private ignored: string[],
private eventCallback: (events: IRawFileChange[]) => void,
private errorCallback: (error: string) => void,
private verboseLogging: boolean
) {
this.restartCounter = 0;
this.startWatcher();
}
private startWatcher(): void {
const args = [this.watchedFolder];
if (this.verboseLogging) {
args.push('-verbose');
}
this.handle = cp.spawn(uri.parse(require.toUrl('vs/workbench/services/files/node/watcher/win32/CodeHelper.exe')).fsPath, args);
const stdoutLineDecoder = new decoder.LineDecoder();
// Events over stdout
this.handle.stdout.on('data', (data: NodeBuffer) => {
// Collect raw events from output
const rawEvents: IRawFileChange[] = [];
stdoutLineDecoder.write(data).forEach((line) => {
const eventParts = line.split('|');
if (eventParts.length === 2) {
const changeType = Number(eventParts[0]);
const absolutePath = eventParts[1];
// File Change Event (0 Changed, 1 Created, 2 Deleted)
if (changeType >= 0 && changeType < 3) {
// Support ignores
if (this.ignored && this.ignored.some(ignore => glob.match(ignore, absolutePath))) {
return;
}
// Otherwise record as event
rawEvents.push({
type: OutOfProcessWin32FolderWatcher.changeTypeMap[changeType],
path: absolutePath
});
}
// 3 Logging
else {
console.log('%c[File Watcher]', 'color: darkgreen', eventParts[1]);
}
}
});
// Trigger processing of events through the delayer to batch them up properly
if (rawEvents.length > 0) {
this.eventCallback(rawEvents);
}
});
// Errors
this.handle.on('error', (error: Error) => this.onError(error));
this.handle.stderr.on('data', (data: NodeBuffer) => this.onError(data));
// Exit
this.handle.on('exit', (code: number, signal: string) => this.onExit(code, signal));
}
private onError(error: Error | NodeBuffer): void {
this.errorCallback('[FileWatcher] process error: ' + error.toString());
}
private onExit(code: number, signal: string): void {
if (this.handle) { // exit while not yet being disposed is unexpected!
this.errorCallback(`[FileWatcher] terminated unexpectedly (code: ${code}, signal: ${signal})`);
if (this.restartCounter <= OutOfProcessWin32FolderWatcher.MAX_RESTARTS) {
this.errorCallback('[FileWatcher] is restarted again...');
this.restartCounter++;
this.startWatcher(); // restart
} else {
this.errorCallback('[FileWatcher] Watcher failed to start after retrying for some time, giving up. Please report this as a bug report!');
}
}
}
public dispose(): void {
if (this.handle) {
this.handle.kill();
this.handle = null;
}
}
}

View File

@@ -0,0 +1,69 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IRawFileChange, toFileChangesEvent } from 'vs/workbench/services/files/node/watcher/common';
import { OutOfProcessWin32FolderWatcher } from 'vs/workbench/services/files/node/watcher/win32/csharpWatcherService';
import { FileChangesEvent } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { normalize } from 'path';
import { rtrim, endsWith } from 'vs/base/common/strings';
import { sep } from 'vs/base/common/paths';
export class FileWatcher {
private isDisposed: boolean;
constructor(
private contextService: IWorkspaceContextService,
private ignored: string[],
private onFileChanges: (changes: FileChangesEvent) => void,
private errorLogger: (msg: string) => void,
private verboseLogging: boolean
) {
}
public startWatching(): () => void {
let basePath: string = normalize(this.contextService.getWorkspace().roots[0].fsPath);
if (basePath && basePath.indexOf('\\\\') === 0 && endsWith(basePath, sep)) {
// for some weird reason, node adds a trailing slash to UNC paths
// we never ever want trailing slashes as our base path unless
// someone opens root ("/").
// See also https://github.com/nodejs/io.js/issues/1765
basePath = rtrim(basePath, sep);
}
const watcher = new OutOfProcessWin32FolderWatcher(
basePath,
this.ignored,
events => this.onRawFileEvents(events),
error => this.onError(error),
this.verboseLogging
);
return () => {
this.isDisposed = true;
watcher.dispose();
};
}
private onRawFileEvents(events: IRawFileChange[]): void {
if (this.isDisposed) {
return;
}
// Emit through event emitter
if (events.length > 0) {
this.onFileChanges(toFileChangesEvent(events));
}
}
private onError(error: string): void {
if (!this.isDisposed) {
this.errorLogger(error);
}
}
}

View File

@@ -0,0 +1,814 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import fs = require('fs');
import path = require('path');
import os = require('os');
import assert = require('assert');
import { TPromise } from 'vs/base/common/winjs.base';
import { FileService, IEncodingOverride } from 'vs/workbench/services/files/node/fileService';
import { FileOperation, FileOperationEvent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files';
import uri from 'vs/base/common/uri';
import uuid = require('vs/base/common/uuid');
import extfs = require('vs/base/node/extfs');
import encodingLib = require('vs/base/node/encoding');
import utils = require('vs/workbench/services/files/test/node/utils');
import { onError } from 'vs/base/test/common/utils';
import { TestContextService } from 'vs/workbench/test/workbenchTestServices';
import { Workspace } from 'vs/platform/workspace/common/workspace';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
suite('FileService', () => {
let service: FileService;
let parentDir = path.join(os.tmpdir(), 'vsctests', 'service');
let testDir: string;
setup(function (done) {
let id = uuid.generateUuid();
testDir = path.join(parentDir, id);
let sourceDir = require.toUrl('./fixtures/service');
extfs.copy(sourceDir, testDir, (error) => {
if (error) {
return onError(error, done);
}
service = new FileService(new TestContextService(new Workspace(testDir, testDir, [uri.file(testDir)])), new TestConfigurationService(), { disableWatcher: true });
done();
});
});
teardown((done) => {
service.dispose();
extfs.del(parentDir, os.tmpdir(), () => { }, done);
});
test('createFile', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const contents = 'Hello World';
const resource = uri.file(path.join(testDir, 'test.txt'));
service.createFile(resource, contents).done(s => {
assert.equal(s.name, 'test.txt');
assert.equal(fs.existsSync(s.resource.fsPath), true);
assert.equal(fs.readFileSync(s.resource.fsPath), contents);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.CREATE);
assert.equal(event.target.resource.fsPath, resource.fsPath);
toDispose.dispose();
done();
}, error => onError(error, done));
});
test('createFolder', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
service.resolveFile(uri.file(testDir)).done(parent => {
const resource = uri.file(path.join(parent.resource.fsPath, 'newFolder'));
return service.createFolder(resource).then(f => {
assert.equal(f.name, 'newFolder');
assert.equal(fs.existsSync(f.resource.fsPath), true);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.CREATE);
assert.equal(event.target.resource.fsPath, resource.fsPath);
assert.equal(event.target.isDirectory, true);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('touchFile', function (done: () => void) {
service.touchFile(uri.file(path.join(testDir, 'test.txt'))).done(s => {
assert.equal(s.name, 'test.txt');
assert.equal(fs.existsSync(s.resource.fsPath), true);
assert.equal(fs.readFileSync(s.resource.fsPath).length, 0);
const stat = fs.statSync(s.resource.fsPath);
return TPromise.timeout(10).then(() => {
return service.touchFile(s.resource).done(s => {
const statNow = fs.statSync(s.resource.fsPath);
assert.ok(statNow.mtime.getTime() >= stat.mtime.getTime()); // one some OS the resolution seems to be 1s, so we use >= here
assert.equal(statNow.size, stat.size);
done();
});
});
}, error => onError(error, done));
});
test('renameFile', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const resource = uri.file(path.join(testDir, 'index.html'));
service.resolveFile(resource).done(source => {
return service.rename(source.resource, 'other.html').then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(fs.existsSync(source.resource.fsPath), false);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.MOVE);
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('renameFolder', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const resource = uri.file(path.join(testDir, 'deep'));
service.resolveFile(resource).done(source => {
return service.rename(source.resource, 'deeper').then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(fs.existsSync(source.resource.fsPath), false);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.MOVE);
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
toDispose.dispose();
done();
});
});
});
test('renameFile - MIX CASE', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const resource = uri.file(path.join(testDir, 'index.html'));
service.resolveFile(resource).done(source => {
return service.rename(source.resource, 'INDEX.html').then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(path.basename(renamed.resource.fsPath), 'INDEX.html');
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.MOVE);
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('moveFile', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const resource = uri.file(path.join(testDir, 'index.html'));
service.resolveFile(resource).done(source => {
return service.moveFile(source.resource, uri.file(path.join(testDir, 'other.html'))).then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(fs.existsSync(source.resource.fsPath), false);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.MOVE);
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('move - FILE_MOVE_CONFLICT', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
service.resolveFile(uri.file(path.join(testDir, 'index.html'))).done(source => {
return service.moveFile(source.resource, uri.file(path.join(testDir, 'binary.txt'))).then(null, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT);
assert.ok(!event);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('moveFile - MIX CASE', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const resource = uri.file(path.join(testDir, 'index.html'));
service.resolveFile(resource).done(source => {
return service.moveFile(source.resource, uri.file(path.join(testDir, 'INDEX.html'))).then(renamed => {
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.equal(path.basename(renamed.resource.fsPath), 'INDEX.html');
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.MOVE);
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('moveFile - overwrite folder with file', function (done: () => void) {
let createEvent: FileOperationEvent;
let moveEvent: FileOperationEvent;
let deleteEvent: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
if (e.operation === FileOperation.CREATE) {
createEvent = e;
} else if (e.operation === FileOperation.DELETE) {
deleteEvent = e;
} else if (e.operation === FileOperation.MOVE) {
moveEvent = e;
}
});
service.resolveFile(uri.file(testDir)).done(parent => {
const folderResource = uri.file(path.join(parent.resource.fsPath, 'conway.js'));
return service.createFolder(folderResource).then(f => {
const resource = uri.file(path.join(testDir, 'deep', 'conway.js'));
return service.moveFile(resource, f.resource, true).then(moved => {
assert.equal(fs.existsSync(moved.resource.fsPath), true);
assert.ok(fs.statSync(moved.resource.fsPath).isFile);
assert.ok(createEvent);
assert.ok(deleteEvent);
assert.ok(moveEvent);
assert.equal(moveEvent.resource.fsPath, resource.fsPath);
assert.equal(moveEvent.target.resource.fsPath, moved.resource.fsPath);
assert.equal(deleteEvent.resource.fsPath, folderResource.fsPath);
toDispose.dispose();
done();
});
});
}, error => onError(error, done));
});
test('copyFile', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
service.resolveFile(uri.file(path.join(testDir, 'index.html'))).done(source => {
const resource = uri.file(path.join(testDir, 'other.html'));
return service.copyFile(source.resource, resource).then(copied => {
assert.equal(fs.existsSync(copied.resource.fsPath), true);
assert.equal(fs.existsSync(source.resource.fsPath), true);
assert.ok(event);
assert.equal(event.resource.fsPath, source.resource.fsPath);
assert.equal(event.operation, FileOperation.COPY);
assert.equal(event.target.resource.fsPath, copied.resource.fsPath);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('copyFile - overwrite folder with file', function (done: () => void) {
let createEvent: FileOperationEvent;
let copyEvent: FileOperationEvent;
let deleteEvent: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
if (e.operation === FileOperation.CREATE) {
createEvent = e;
} else if (e.operation === FileOperation.DELETE) {
deleteEvent = e;
} else if (e.operation === FileOperation.COPY) {
copyEvent = e;
}
});
service.resolveFile(uri.file(testDir)).done(parent => {
const folderResource = uri.file(path.join(parent.resource.fsPath, 'conway.js'));
return service.createFolder(folderResource).then(f => {
const resource = uri.file(path.join(testDir, 'deep', 'conway.js'));
return service.copyFile(resource, f.resource, true).then(copied => {
assert.equal(fs.existsSync(copied.resource.fsPath), true);
assert.ok(fs.statSync(copied.resource.fsPath).isFile);
assert.ok(createEvent);
assert.ok(deleteEvent);
assert.ok(copyEvent);
assert.equal(copyEvent.resource.fsPath, resource.fsPath);
assert.equal(copyEvent.target.resource.fsPath, copied.resource.fsPath);
assert.equal(deleteEvent.resource.fsPath, folderResource.fsPath);
toDispose.dispose();
done();
});
});
}, error => onError(error, done));
});
test('importFile', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
service.resolveFile(uri.file(path.join(testDir, 'deep'))).done(target => {
const resource = uri.file(require.toUrl('./fixtures/service/index.html'));
return service.importFile(resource, target.resource).then(res => {
assert.equal(res.isNew, true);
assert.equal(fs.existsSync(res.stat.resource.fsPath), true);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.IMPORT);
assert.equal(event.target.resource.fsPath, res.stat.resource.fsPath);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('importFile - MIX CASE', function (done: () => void) {
service.resolveFile(uri.file(path.join(testDir, 'index.html'))).done(source => {
return service.rename(source.resource, 'CONWAY.js').then(renamed => { // index.html => CONWAY.js
assert.equal(fs.existsSync(renamed.resource.fsPath), true);
assert.ok(fs.readdirSync(testDir).some(f => f === 'CONWAY.js'));
return service.resolveFile(uri.file(path.join(testDir, 'deep', 'conway.js'))).done(source => {
return service.importFile(source.resource, uri.file(testDir)).then(res => { // CONWAY.js => conway.js
assert.equal(fs.existsSync(res.stat.resource.fsPath), true);
assert.ok(fs.readdirSync(testDir).some(f => f === 'conway.js'));
done();
});
});
});
}, error => onError(error, done));
});
test('importFile - overwrite folder with file', function (done: () => void) {
let createEvent: FileOperationEvent;
let importEvent: FileOperationEvent;
let deleteEvent: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
if (e.operation === FileOperation.CREATE) {
createEvent = e;
} else if (e.operation === FileOperation.DELETE) {
deleteEvent = e;
} else if (e.operation === FileOperation.IMPORT) {
importEvent = e;
}
});
service.resolveFile(uri.file(testDir)).done(parent => {
const folderResource = uri.file(path.join(parent.resource.fsPath, 'conway.js'));
return service.createFolder(folderResource).then(f => {
const resource = uri.file(path.join(testDir, 'deep', 'conway.js'));
return service.importFile(resource, uri.file(testDir)).then(res => {
assert.equal(fs.existsSync(res.stat.resource.fsPath), true);
assert.ok(fs.readdirSync(testDir).some(f => f === 'conway.js'));
assert.ok(fs.statSync(res.stat.resource.fsPath).isFile);
assert.ok(createEvent);
assert.ok(deleteEvent);
assert.ok(importEvent);
assert.equal(importEvent.resource.fsPath, resource.fsPath);
assert.equal(importEvent.target.resource.fsPath, res.stat.resource.fsPath);
assert.equal(deleteEvent.resource.fsPath, folderResource.fsPath);
toDispose.dispose();
done();
});
});
}, error => onError(error, done));
});
test('importFile - same file', function (done: () => void) {
service.resolveFile(uri.file(path.join(testDir, 'index.html'))).done(source => {
return service.importFile(source.resource, uri.file(path.dirname(source.resource.fsPath))).then(imported => {
assert.equal(imported.stat.size, source.size);
done();
});
}, error => onError(error, done));
});
test('deleteFile', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const resource = uri.file(path.join(testDir, 'deep', 'conway.js'));
service.resolveFile(resource).done(source => {
return service.del(source.resource).then(() => {
assert.equal(fs.existsSync(source.resource.fsPath), false);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.DELETE);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('deleteFolder', function (done: () => void) {
let event: FileOperationEvent;
const toDispose = service.onAfterOperation(e => {
event = e;
});
const resource = uri.file(path.join(testDir, 'deep'));
service.resolveFile(resource).done(source => {
return service.del(source.resource).then(() => {
assert.equal(fs.existsSync(source.resource.fsPath), false);
assert.ok(event);
assert.equal(event.resource.fsPath, resource.fsPath);
assert.equal(event.operation, FileOperation.DELETE);
toDispose.dispose();
done();
});
}, error => onError(error, done));
});
test('resolveFile', function (done: () => void) {
service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))] }).done(r => {
assert.equal(r.children.length, 6);
let deep = utils.getByName(r, 'deep');
assert.equal(deep.children.length, 4);
done();
}, error => onError(error, done));
});
test('resolveFiles', function (done: () => void) {
service.resolveFiles([
{ resource: uri.file(testDir), options: { resolveTo: [uri.file(path.join(testDir, 'deep'))] } },
{ resource: uri.file(path.join(testDir, 'deep')) }
]).then(res => {
const r1 = res[0].stat;
assert.equal(r1.children.length, 6);
let deep = utils.getByName(r1, 'deep');
assert.equal(deep.children.length, 4);
const r2 = res[1].stat;
assert.equal(r2.children.length, 4);
assert.equal(r2.name, 'deep');
done();
}, error => onError(error, done));
});
test('existsFile', function (done: () => void) {
service.existsFile(uri.file(testDir)).then((exists) => {
assert.equal(exists, true);
service.existsFile(uri.file(testDir + 'something')).then((exists) => {
assert.equal(exists, false);
done();
});
}, error => onError(error, done));
});
test('updateContent', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'small.txt'));
service.resolveContent(resource).done(c => {
assert.equal(c.value, 'Small File');
c.value = 'Updates to the small file';
return service.updateContent(c.resource, c.value).then(c => {
assert.equal(fs.readFileSync(resource.fsPath), 'Updates to the small file');
done();
});
}, error => onError(error, done));
});
test('updateContent - use encoding (UTF 16 BE)', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'small.txt'));
let encoding = 'utf16be';
service.resolveContent(resource).done(c => {
c.encoding = encoding;
return service.updateContent(c.resource, c.value, { encoding: encoding }).then(c => {
return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => {
assert.equal(enc, encodingLib.UTF16be);
return service.resolveContent(resource).then(c => {
assert.equal(c.encoding, encoding);
done();
});
});
});
}, error => onError(error, done));
});
test('updateContent - encoding preserved (UTF 16 LE)', function (done: () => void) {
let encoding = 'utf16le';
let resource = uri.file(path.join(testDir, 'some_utf16le.css'));
service.resolveContent(resource).done(c => {
assert.equal(c.encoding, encoding);
c.value = 'Some updates';
return service.updateContent(c.resource, c.value, { encoding: encoding }).then(c => {
return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => {
assert.equal(enc, encodingLib.UTF16le);
return service.resolveContent(resource).then(c => {
assert.equal(c.encoding, encoding);
done();
});
});
});
}, error => onError(error, done));
});
test('resolveContent - FILE_IS_BINARY', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'binary.txt'));
service.resolveContent(resource, { acceptTextOnly: true }).done(null, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_IS_BINARY);
return service.resolveContent(uri.file(path.join(testDir, 'small.txt')), { acceptTextOnly: true }).then(r => {
assert.equal(r.name, 'small.txt');
done();
});
}, error => onError(error, done));
});
test('resolveContent - FILE_IS_DIRECTORY', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'deep'));
service.resolveContent(resource).done(null, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY);
done();
}, error => onError(error, done));
});
test('resolveContent - FILE_NOT_FOUND', function (done: () => void) {
let resource = uri.file(path.join(testDir, '404.html'));
service.resolveContent(resource).done(null, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
done();
}, error => onError(error, done));
});
test('resolveContent - FILE_NOT_MODIFIED_SINCE', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'index.html'));
service.resolveContent(resource).done(c => {
return service.resolveContent(resource, { etag: c.etag }).then(null, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE);
done();
});
}, error => onError(error, done));
});
test('resolveContent - FILE_MODIFIED_SINCE', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'index.html'));
service.resolveContent(resource).done(c => {
fs.writeFileSync(resource.fsPath, 'Updates Incoming!');
return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(null, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE);
done();
});
}, error => onError(error, done));
});
test('resolveContent - encoding picked up', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'index.html'));
let encoding = 'windows1252';
service.resolveContent(resource, { encoding: encoding }).done(c => {
assert.equal(c.encoding, encoding);
done();
}, error => onError(error, done));
});
test('resolveContent - user overrides BOM', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'some_utf16le.css'));
service.resolveContent(resource, { encoding: 'windows1252' }).done(c => {
assert.equal(c.encoding, 'windows1252');
done();
}, error => onError(error, done));
});
test('resolveContent - BOM removed', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'some_utf8_bom.txt'));
service.resolveContent(resource).done(c => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(new Buffer(c.value), 512), null);
done();
}, error => onError(error, done));
});
test('resolveContent - invalid encoding', function (done: () => void) {
let resource = uri.file(path.join(testDir, 'index.html'));
service.resolveContent(resource, { encoding: 'superduper' }).done(c => {
assert.equal(c.encoding, 'utf8');
done();
}, error => onError(error, done));
});
test('watchFileChanges', function (done: () => void) {
let toWatch = uri.file(path.join(testDir, 'index.html'));
service.watchFileChanges(toWatch);
service.onFileChanges((e: FileChangesEvent) => {
assert.ok(e);
service.unwatchFileChanges(toWatch);
done();
});
setTimeout(() => {
fs.writeFileSync(toWatch.fsPath, 'Changes');
}, 100);
});
test('watchFileChanges - support atomic save', function (done: () => void) {
let toWatch = uri.file(path.join(testDir, 'index.html'));
service.watchFileChanges(toWatch);
service.onFileChanges((e: FileChangesEvent) => {
assert.ok(e);
service.unwatchFileChanges(toWatch);
done();
});
setTimeout(() => {
// Simulate atomic save by deleting the file, creating it under different name
// and then replacing the previously deleted file with those contents
const renamed = `${toWatch.fsPath}.bak`;
fs.unlinkSync(toWatch.fsPath);
fs.writeFileSync(renamed, 'Changes');
fs.renameSync(renamed, toWatch.fsPath);
}, 100);
});
test('options - encoding', function (done: () => void) {
// setup
let _id = uuid.generateUuid();
let _testDir = path.join(parentDir, _id);
let _sourceDir = require.toUrl('./fixtures/service');
extfs.copy(_sourceDir, _testDir, () => {
let encodingOverride: IEncodingOverride[] = [];
encodingOverride.push({
resource: uri.file(path.join(testDir, 'deep')),
encoding: 'utf16le'
});
let configurationService = new TestConfigurationService();
configurationService.setUserConfiguration('files', { encoding: 'windows1252' });
let _service = new FileService(new TestContextService(new Workspace(_testDir, _testDir, [uri.file(_testDir)])), configurationService, {
encodingOverride,
disableWatcher: true
});
_service.resolveContent(uri.file(path.join(testDir, 'index.html'))).done(c => {
assert.equal(c.encoding, 'windows1252');
return _service.resolveContent(uri.file(path.join(testDir, 'deep', 'conway.js'))).done(c => {
assert.equal(c.encoding, 'utf16le');
// teardown
_service.dispose();
done();
});
});
});
});
test('UTF 8 BOMs', function (done: () => void) {
// setup
let _id = uuid.generateUuid();
let _testDir = path.join(parentDir, _id);
let _sourceDir = require.toUrl('./fixtures/service');
let resource = uri.file(path.join(testDir, 'index.html'));
let _service = new FileService(new TestContextService(new Workspace(_testDir, _testDir, [uri.file(_testDir)])), new TestConfigurationService(), {
disableWatcher: true
});
extfs.copy(_sourceDir, _testDir, () => {
fs.readFile(resource.fsPath, (error, data) => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null);
// Update content: UTF_8 => UTF_8_BOM
_service.updateContent(resource, 'Hello Bom', { encoding: encodingLib.UTF8_with_bom }).done(() => {
fs.readFile(resource.fsPath, (error, data) => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8);
// Update content: PRESERVE BOM when using UTF-8
_service.updateContent(resource, 'Please stay Bom', { encoding: encodingLib.UTF8 }).done(() => {
fs.readFile(resource.fsPath, (error, data) => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8);
// Update content: REMOVE BOM
_service.updateContent(resource, 'Go away Bom', { encoding: encodingLib.UTF8, overwriteEncoding: true }).done(() => {
fs.readFile(resource.fsPath, (error, data) => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null);
// Update content: BOM comes not back
_service.updateContent(resource, 'Do not come back Bom', { encoding: encodingLib.UTF8 }).done(() => {
fs.readFile(resource.fsPath, (error, data) => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null);
_service.dispose();
done();
});
});
});
});
});
});
});
});
});
});
});
});

View File

@@ -0,0 +1,23 @@
'use strict';
/// <reference path="employee.ts" />
var Workforce;
(function (Workforce_1) {
var Company = (function () {
function Company() {
}
return Company;
})();
(function (property, Workforce, IEmployee) {
if (property === void 0) { property = employees; }
if (IEmployee === void 0) { IEmployee = []; }
property;
calculateMonthlyExpenses();
{
var result = 0;
for (var i = 0; i < employees.length; i++) {
result += employees[i].calculatePay();
}
return result;
}
});
})(Workforce || (Workforce = {}));

View File

@@ -0,0 +1,117 @@
'use strict';
var Conway;
(function (Conway) {
var Cell = (function () {
function Cell() {
}
return Cell;
})();
(function (property, number, property, number, property, boolean) {
if (property === void 0) { property = row; }
if (property === void 0) { property = col; }
if (property === void 0) { property = live; }
});
var GameOfLife = (function () {
function GameOfLife() {
}
return GameOfLife;
})();
(function () {
property;
gridSize = 50;
property;
canvasSize = 600;
property;
lineColor = '#cdcdcd';
property;
liveColor = '#666';
property;
deadColor = '#eee';
property;
initialLifeProbability = 0.5;
property;
animationRate = 60;
property;
cellSize = 0;
property;
context: ICanvasRenderingContext2D;
property;
world = createWorld();
circleOfLife();
function createWorld() {
return travelWorld(function (cell) {
cell.live = Math.random() < initialLifeProbability;
return cell;
});
}
function circleOfLife() {
world = travelWorld(function (cell) {
cell = world[cell.row][cell.col];
draw(cell);
return resolveNextGeneration(cell);
});
setTimeout(function () { circleOfLife(); }, animationRate);
}
function resolveNextGeneration(cell) {
var count = countNeighbors(cell);
var newCell = new Cell(cell.row, cell.col, cell.live);
if (count < 2 || count > 3)
newCell.live = false;
else if (count == 3)
newCell.live = true;
return newCell;
}
function countNeighbors(cell) {
var neighbors = 0;
for (var row = -1; row <= 1; row++) {
for (var col = -1; col <= 1; col++) {
if (row == 0 && col == 0)
continue;
if (isAlive(cell.row + row, cell.col + col)) {
neighbors++;
}
}
}
return neighbors;
}
function isAlive(row, col) {
// todo - need to guard with worl[row] exists?
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
return false;
return world[row][col].live;
}
function travelWorld(callback) {
var result = [];
for (var row = 0; row < gridSize; row++) {
var rowData = [];
for (var col = 0; col < gridSize; col++) {
rowData.push(callback(new Cell(row, col, false)));
}
result.push(rowData);
}
return result;
}
function draw(cell) {
if (context == null)
context = createDrawingContext();
if (cellSize == 0)
cellSize = canvasSize / gridSize;
context.strokeStyle = lineColor;
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
context.fillStyle = cell.live ? liveColor : deadColor;
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
}
function createDrawingContext() {
var canvas = document.getElementById('conway-canvas');
if (canvas == null) {
canvas = document.createElement('canvas');
canvas.id = "conway-canvas";
canvas.width = canvasSize;
canvas.height = canvasSize;
document.body.appendChild(canvas);
}
return canvas.getContext('2d');
}
});
})(Conway || (Conway = {}));
var game = new Conway.GameOfLife();

View File

@@ -0,0 +1,38 @@
'use strict';
var Workforce;
(function (Workforce) {
var Employee = (function () {
function Employee() {
}
return Employee;
})();
(property);
name: string, property;
basepay: number;
implements;
IEmployee;
{
name;
basepay;
}
var SalesEmployee = (function () {
function SalesEmployee() {
}
return SalesEmployee;
})();
();
Employee(name, basepay);
{
function calculatePay() {
var multiplier = (document.getElementById("mult")), as = any, value;
return _super.calculatePay.call(this) * multiplier + bonus;
}
}
var employee = new Employee('Bob', 1000);
var salesEmployee = new SalesEmployee('Jim', 800, 400);
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
})(Workforce || (Workforce = {}));
extern;
var $;
var s = Workforce.salesEmployee.calculatePay();
$('#results').text(s);

View File

@@ -0,0 +1,24 @@
'use strict';
var M;
(function (M) {
var C = (function () {
function C() {
}
return C;
})();
(function (x, property, number) {
if (property === void 0) { property = w; }
var local = 1;
// unresolved symbol because x is local
//self.x++;
self.w--; // ok because w is a property
property;
f = function (y) {
return y + x + local + w + self.w;
};
function sum(z) {
return z + f(z) + w + self.w;
}
});
})(M || (M = {}));
var c = new M.C(12, 5);

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head id='headID'>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Strada </title>
<link href="site.css" rel="stylesheet" type="text/css" />
<script src="jquery-1.4.1.js"></script>
<script src="../compiler/dtree.js" type="text/javascript"></script>
<script src="../compiler/typescript.js" type="text/javascript"></script>
<script type="text/javascript">
// Compile strada source into resulting javascript
function compile(prog, libText) {
var outfile = {
source: "",
Write: function (s) { this.source += s; },
WriteLine: function (s) { this.source += s + "\r"; },
}
var parseErrors = []
var compiler=new Tools.TypeScriptCompiler(outfile,true);
compiler.setErrorCallback(function(start,len, message) { parseErrors.push({start:start, len:len, message:message}); });
compiler.addUnit(libText,"lib.ts");
compiler.addUnit(prog,"input.ts");
compiler.typeCheck();
compiler.emit();
if(parseErrors.length > 0 ) {
//throw new Error(parseErrors);
}
while(outfile.source[0] == '/' && outfile.source[1] == '/' && outfile.source[2] == ' ') {
outfile.source = outfile.source.slice(outfile.source.indexOf('\r')+1);
}
var errorPrefix = "";
for(var i = 0;i<parseErrors.length;i++) {
errorPrefix += "// Error: (" + parseErrors[i].start + "," + parseErrors[i].len + ") " + parseErrors[i].message + "\r";
}
return errorPrefix + outfile.source;
}
</script>
<script type="text/javascript">
var libText = "";
$.get("../compiler/lib.ts", function(newLibText) {
libText = newLibText;
});
// execute the javascript in the compiledOutput pane
function execute() {
$('#compilation').text("Running...");
var txt = $('#compiledOutput').val();
var res;
try {
var ret = eval(txt);
res = "Ran successfully!";
} catch(e) {
res = "Exception thrown: " + e;
}
$('#compilation').text(String(res));
}
// recompile the stradaSrc and populate the compiledOutput pane
function srcUpdated() {
var newText = $('#stradaSrc').val();
var compiledSource;
try {
compiledSource = compile(newText, libText);
} catch (e) {
compiledSource = "//Parse error"
for(var i in e)
compiledSource += "\r// " + e[i];
}
$('#compiledOutput').val(compiledSource);
}
// Populate the stradaSrc pane with one of the built in samples
function exampleSelectionChanged() {
var examples = document.getElementById('examples');
var selectedExample = examples.options[examples.selectedIndex].value;
if (selectedExample != "") {
$.get('examples/' + selectedExample, function (srcText) {
$('#stradaSrc').val(srcText);
setTimeout(srcUpdated,100);
}, function (err) {
console.log(err);
});
}
}
</script>
</head>
<body>
<h1>TypeScript</h1>
<br />
<select id="examples" onchange='exampleSelectionChanged()'>
<option value="">Select...</option>
<option value="small.ts">Small</option>
<option value="employee.ts">Employees</option>
<option value="conway.ts">Conway Game of Life</option>
<option value="typescript.ts">TypeScript Compiler</option>
</select>
<div>
<textarea id='stradaSrc' rows='40' cols='80' onchange='srcUpdated()' onkeyup='srcUpdated()' spellcheck="false">
//Type your TypeScript here...
</textarea>
<textarea id='compiledOutput' rows='40' cols='80' spellcheck="false">
//Compiled code will show up here...
</textarea>
<br />
<button onclick='execute()'/>Run</button>
<div id='compilation'>Press 'run' to execute code...</div>
<div id='results'>...write your results into #results...</div>
</div>
<div id='bod' style='display:none'></div>
</body>
</html>

View File

@@ -0,0 +1,23 @@
'use strict';
/// <reference path="employee.ts" />
var Workforce;
(function (Workforce_1) {
var Company = (function () {
function Company() {
}
return Company;
})();
(function (property, Workforce, IEmployee) {
if (property === void 0) { property = employees; }
if (IEmployee === void 0) { IEmployee = []; }
property;
calculateMonthlyExpenses();
{
var result = 0;
for (var i = 0; i < employees.length; i++) {
result += employees[i].calculatePay();
}
return result;
}
});
})(Workforce || (Workforce = {}));

View File

@@ -0,0 +1,117 @@
'use strict';
var Conway;
(function (Conway) {
var Cell = (function () {
function Cell() {
}
return Cell;
})();
(function (property, number, property, number, property, boolean) {
if (property === void 0) { property = row; }
if (property === void 0) { property = col; }
if (property === void 0) { property = live; }
});
var GameOfLife = (function () {
function GameOfLife() {
}
return GameOfLife;
})();
(function () {
property;
gridSize = 50;
property;
canvasSize = 600;
property;
lineColor = '#cdcdcd';
property;
liveColor = '#666';
property;
deadColor = '#eee';
property;
initialLifeProbability = 0.5;
property;
animationRate = 60;
property;
cellSize = 0;
property;
context: ICanvasRenderingContext2D;
property;
world = createWorld();
circleOfLife();
function createWorld() {
return travelWorld(function (cell) {
cell.live = Math.random() < initialLifeProbability;
return cell;
});
}
function circleOfLife() {
world = travelWorld(function (cell) {
cell = world[cell.row][cell.col];
draw(cell);
return resolveNextGeneration(cell);
});
setTimeout(function () { circleOfLife(); }, animationRate);
}
function resolveNextGeneration(cell) {
var count = countNeighbors(cell);
var newCell = new Cell(cell.row, cell.col, cell.live);
if (count < 2 || count > 3)
newCell.live = false;
else if (count == 3)
newCell.live = true;
return newCell;
}
function countNeighbors(cell) {
var neighbors = 0;
for (var row = -1; row <= 1; row++) {
for (var col = -1; col <= 1; col++) {
if (row == 0 && col == 0)
continue;
if (isAlive(cell.row + row, cell.col + col)) {
neighbors++;
}
}
}
return neighbors;
}
function isAlive(row, col) {
// todo - need to guard with worl[row] exists?
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
return false;
return world[row][col].live;
}
function travelWorld(callback) {
var result = [];
for (var row = 0; row < gridSize; row++) {
var rowData = [];
for (var col = 0; col < gridSize; col++) {
rowData.push(callback(new Cell(row, col, false)));
}
result.push(rowData);
}
return result;
}
function draw(cell) {
if (context == null)
context = createDrawingContext();
if (cellSize == 0)
cellSize = canvasSize / gridSize;
context.strokeStyle = lineColor;
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
context.fillStyle = cell.live ? liveColor : deadColor;
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
}
function createDrawingContext() {
var canvas = document.getElementById('conway-canvas');
if (canvas == null) {
canvas = document.createElement('canvas');
canvas.id = "conway-canvas";
canvas.width = canvasSize;
canvas.height = canvasSize;
document.body.appendChild(canvas);
}
return canvas.getContext('2d');
}
});
})(Conway || (Conway = {}));
var game = new Conway.GameOfLife();

View File

@@ -0,0 +1,38 @@
'use strict';
var Workforce;
(function (Workforce) {
var Employee = (function () {
function Employee() {
}
return Employee;
})();
(property);
name: string, property;
basepay: number;
implements;
IEmployee;
{
name;
basepay;
}
var SalesEmployee = (function () {
function SalesEmployee() {
}
return SalesEmployee;
})();
();
Employee(name, basepay);
{
function calculatePay() {
var multiplier = (document.getElementById("mult")), as = any, value;
return _super.calculatePay.call(this) * multiplier + bonus;
}
}
var employee = new Employee('Bob', 1000);
var salesEmployee = new SalesEmployee('Jim', 800, 400);
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
})(Workforce || (Workforce = {}));
extern;
var $;
var s = Workforce.salesEmployee.calculatePay();
$('#results').text(s);

View File

@@ -0,0 +1,24 @@
'use strict';
var M;
(function (M) {
var C = (function () {
function C() {
}
return C;
})();
(function (x, property, number) {
if (property === void 0) { property = w; }
var local = 1;
// unresolved symbol because x is local
//self.x++;
self.w--; // ok because w is a property
property;
f = function (y) {
return y + x + local + w + self.w;
};
function sum(z) {
return z + f(z) + w + self.w;
}
});
})(M || (M = {}));
var c = new M.C(12, 5);

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------
The base color for this template is #5c87b2. If you'd like
to use a different color start by replacing all instances of
#5c87b2 with your new color.
----------------------------------------------------------*/
body
{
background-color: #5c87b2;
font-size: .75em;
font-family: Segoe UI, Verdana, Helvetica, Sans-Serif;
margin: 8px;
padding: 0;
color: #696969;
}
h1, h2, h3, h4, h5, h6
{
color: #000;
font-size: 40px;
margin: 0px;
}
textarea
{
font-family: Consolas
}
#results
{
margin-top: 2em;
margin-left: 2em;
color: black;
font-size: medium;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

View File

@@ -0,0 +1,23 @@
'use strict';
/// <reference path="employee.ts" />
var Workforce;
(function (Workforce_1) {
var Company = (function () {
function Company() {
}
return Company;
})();
(function (property, Workforce, IEmployee) {
if (property === void 0) { property = employees; }
if (IEmployee === void 0) { IEmployee = []; }
property;
calculateMonthlyExpenses();
{
var result = 0;
for (var i = 0; i < employees.length; i++) {
result += employees[i].calculatePay();
}
return result;
}
});
})(Workforce || (Workforce = {}));

View File

@@ -0,0 +1,117 @@
'use strict';
var Conway;
(function (Conway) {
var Cell = (function () {
function Cell() {
}
return Cell;
})();
(function (property, number, property, number, property, boolean) {
if (property === void 0) { property = row; }
if (property === void 0) { property = col; }
if (property === void 0) { property = live; }
});
var GameOfLife = (function () {
function GameOfLife() {
}
return GameOfLife;
})();
(function () {
property;
gridSize = 50;
property;
canvasSize = 600;
property;
lineColor = '#cdcdcd';
property;
liveColor = '#666';
property;
deadColor = '#eee';
property;
initialLifeProbability = 0.5;
property;
animationRate = 60;
property;
cellSize = 0;
property;
context: ICanvasRenderingContext2D;
property;
world = createWorld();
circleOfLife();
function createWorld() {
return travelWorld(function (cell) {
cell.live = Math.random() < initialLifeProbability;
return cell;
});
}
function circleOfLife() {
world = travelWorld(function (cell) {
cell = world[cell.row][cell.col];
draw(cell);
return resolveNextGeneration(cell);
});
setTimeout(function () { circleOfLife(); }, animationRate);
}
function resolveNextGeneration(cell) {
var count = countNeighbors(cell);
var newCell = new Cell(cell.row, cell.col, cell.live);
if (count < 2 || count > 3)
newCell.live = false;
else if (count == 3)
newCell.live = true;
return newCell;
}
function countNeighbors(cell) {
var neighbors = 0;
for (var row = -1; row <= 1; row++) {
for (var col = -1; col <= 1; col++) {
if (row == 0 && col == 0)
continue;
if (isAlive(cell.row + row, cell.col + col)) {
neighbors++;
}
}
}
return neighbors;
}
function isAlive(row, col) {
// todo - need to guard with worl[row] exists?
if (row < 0 || col < 0 || row >= gridSize || col >= gridSize)
return false;
return world[row][col].live;
}
function travelWorld(callback) {
var result = [];
for (var row = 0; row < gridSize; row++) {
var rowData = [];
for (var col = 0; col < gridSize; col++) {
rowData.push(callback(new Cell(row, col, false)));
}
result.push(rowData);
}
return result;
}
function draw(cell) {
if (context == null)
context = createDrawingContext();
if (cellSize == 0)
cellSize = canvasSize / gridSize;
context.strokeStyle = lineColor;
context.strokeRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
context.fillStyle = cell.live ? liveColor : deadColor;
context.fillRect(cell.row * cellSize, cell.col * cellSize, cellSize, cellSize);
}
function createDrawingContext() {
var canvas = document.getElementById('conway-canvas');
if (canvas == null) {
canvas = document.createElement('canvas');
canvas.id = "conway-canvas";
canvas.width = canvasSize;
canvas.height = canvasSize;
document.body.appendChild(canvas);
}
return canvas.getContext('2d');
}
});
})(Conway || (Conway = {}));
var game = new Conway.GameOfLife();

View File

@@ -0,0 +1,38 @@
'use strict';
var Workforce;
(function (Workforce) {
var Employee = (function () {
function Employee() {
}
return Employee;
})();
(property);
name: string, property;
basepay: number;
implements;
IEmployee;
{
name;
basepay;
}
var SalesEmployee = (function () {
function SalesEmployee() {
}
return SalesEmployee;
})();
();
Employee(name, basepay);
{
function calculatePay() {
var multiplier = (document.getElementById("mult")), as = any, value;
return _super.calculatePay.call(this) * multiplier + bonus;
}
}
var employee = new Employee('Bob', 1000);
var salesEmployee = new SalesEmployee('Jim', 800, 400);
salesEmployee.calclatePay(); // error: No member 'calclatePay' on SalesEmployee
})(Workforce || (Workforce = {}));
extern;
var $;
var s = Workforce.salesEmployee.calculatePay();
$('#results').text(s);

View File

@@ -0,0 +1,24 @@
'use strict';
var M;
(function (M) {
var C = (function () {
function C() {
}
return C;
})();
(function (x, property, number) {
if (property === void 0) { property = w; }
var local = 1;
// unresolved symbol because x is local
//self.x++;
self.w--; // ok because w is a property
property;
f = function (y) {
return y + x + local + w + self.w;
};
function sum(z) {
return z + f(z) + w + self.w;
}
});
})(M || (M = {}));
var c = new M.C(12, 5);

View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html>
<head id='headID'>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Strada </title>
<link href="site.css" rel="stylesheet" type="text/css" />
<script src="jquery-1.4.1.js"></script>
<script src="../compiler/dtree.js" type="text/javascript"></script>
<script src="../compiler/typescript.js" type="text/javascript"></script>
<script type="text/javascript">
// Compile strada source into resulting javascript
function compile(prog, libText) {
var outfile = {
source: "",
Write: function (s) { this.source += s; },
WriteLine: function (s) { this.source += s + "\r"; },
}
var parseErrors = []
var compiler=new Tools.TypeScriptCompiler(outfile,true);
compiler.setErrorCallback(function(start,len, message) { parseErrors.push({start:start, len:len, message:message}); });
compiler.addUnit(libText,"lib.ts");
compiler.addUnit(prog,"input.ts");
compiler.typeCheck();
compiler.emit();
if(parseErrors.length > 0 ) {
//throw new Error(parseErrors);
}
while(outfile.source[0] == '/' && outfile.source[1] == '/' && outfile.source[2] == ' ') {
outfile.source = outfile.source.slice(outfile.source.indexOf('\r')+1);
}
var errorPrefix = "";
for(var i = 0;i<parseErrors.length;i++) {
errorPrefix += "// Error: (" + parseErrors[i].start + "," + parseErrors[i].len + ") " + parseErrors[i].message + "\r";
}
return errorPrefix + outfile.source;
}
</script>
<script type="text/javascript">
var libText = "";
$.get("../compiler/lib.ts", function(newLibText) {
libText = newLibText;
});
// execute the javascript in the compiledOutput pane
function execute() {
$('#compilation').text("Running...");
var txt = $('#compiledOutput').val();
var res;
try {
var ret = eval(txt);
res = "Ran successfully!";
} catch(e) {
res = "Exception thrown: " + e;
}
$('#compilation').text(String(res));
}
// recompile the stradaSrc and populate the compiledOutput pane
function srcUpdated() {
var newText = $('#stradaSrc').val();
var compiledSource;
try {
compiledSource = compile(newText, libText);
} catch (e) {
compiledSource = "//Parse error"
for(var i in e)
compiledSource += "\r// " + e[i];
}
$('#compiledOutput').val(compiledSource);
}
// Populate the stradaSrc pane with one of the built in samples
function exampleSelectionChanged() {
var examples = document.getElementById('examples');
var selectedExample = examples.options[examples.selectedIndex].value;
if (selectedExample != "") {
$.get('examples/' + selectedExample, function (srcText) {
$('#stradaSrc').val(srcText);
setTimeout(srcUpdated,100);
}, function (err) {
console.log(err);
});
}
}
</script>
</head>
<body>
<h1>TypeScript</h1>
<br />
<select id="examples" onchange='exampleSelectionChanged()'>
<option value="">Select...</option>
<option value="small.ts">Small</option>
<option value="employee.ts">Employees</option>
<option value="conway.ts">Conway Game of Life</option>
<option value="typescript.ts">TypeScript Compiler</option>
</select>
<div>
<textarea id='stradaSrc' rows='40' cols='80' onchange='srcUpdated()' onkeyup='srcUpdated()' spellcheck="false">
//Type your TypeScript here...
</textarea>
<textarea id='compiledOutput' rows='40' cols='80' spellcheck="false">
//Compiled code will show up here...
</textarea>
<br />
<button onclick='execute()'/>Run</button>
<div id='compilation'>Press 'run' to execute code...</div>
<div id='results'>...write your results into #results...</div>
</div>
<div id='bod' style='display:none'></div>
</body>
</html>

View File

@@ -0,0 +1 @@
Small File

View File

@@ -0,0 +1 @@
This is some UTF 8 with BOM file.

View File

@@ -0,0 +1,188 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import fs = require('fs');
import path = require('path');
import assert = require('assert');
import { StatResolver } from 'vs/workbench/services/files/node/fileService';
import uri from 'vs/base/common/uri';
import { isLinux } from 'vs/base/common/platform';
import utils = require('vs/workbench/services/files/test/node/utils');
function create(relativePath: string): StatResolver {
let basePath = require.toUrl('./fixtures/resolver');
let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath;
let fsStat = fs.statSync(absolutePath);
return new StatResolver(uri.file(absolutePath), fsStat.isDirectory(), fsStat.mtime.getTime(), fsStat.size, false);
}
function toResource(relativePath: string): uri {
let basePath = require.toUrl('./fixtures/resolver');
let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath;
return uri.file(absolutePath);
}
suite('Stat Resolver', () => {
test('resolve file', function (done: () => void) {
let resolver = create('/index.html');
resolver.resolve(null).then(result => {
assert.ok(!result.isDirectory);
assert.equal(result.name, 'index.html');
assert.ok(!!result.etag);
resolver = create('examples');
return resolver.resolve(null).then(result => {
assert.ok(result.isDirectory);
});
})
.done(() => done(), done);
});
test('resolve directory', function (done: () => void) {
let testsElements = ['examples', 'other', 'index.html', 'site.css'];
let resolver = create('/');
resolver.resolve(null).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.hasChildren);
assert.ok(result.isDirectory);
assert.equal(result.children.length, testsElements.length);
assert.ok(result.children.every((entry) => {
return testsElements.some((name) => {
return path.basename(entry.resource.fsPath) === name;
});
}));
result.children.forEach((value) => {
assert.ok(path.basename(value.resource.fsPath));
if (['examples', 'other'].indexOf(path.basename(value.resource.fsPath)) >= 0) {
assert.ok(value.isDirectory);
assert.ok(value.hasChildren);
} else if (path.basename(value.resource.fsPath) === 'index.html') {
assert.ok(!value.isDirectory);
assert.ok(value.hasChildren === false);
} else if (path.basename(value.resource.fsPath) === 'site.css') {
assert.ok(!value.isDirectory);
assert.ok(value.hasChildren === false);
} else {
assert.ok(!'Unexpected value ' + path.basename(value.resource.fsPath));
}
});
})
.done(() => done(), done);
});
test('resolve directory - resolveTo single directory', function (done: () => void) {
let resolver = create('/');
resolver.resolve({ resolveTo: [toResource('other/deep')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.hasChildren);
assert.ok(result.isDirectory);
let children = result.children;
assert.equal(children.length, 4);
let other = utils.getByName(result, 'other');
assert.ok(other);
assert.ok(other.hasChildren);
let deep = utils.getByName(other, 'deep');
assert.ok(deep);
assert.ok(deep.hasChildren);
assert.equal(deep.children.length, 4);
})
.done(() => done(), done);
});
test('resolve directory - resolveTo single directory - mixed casing', function (done: () => void) {
let resolver = create('/');
resolver.resolve({ resolveTo: [toResource('other/Deep')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.hasChildren);
assert.ok(result.isDirectory);
let children = result.children;
assert.equal(children.length, 4);
let other = utils.getByName(result, 'other');
assert.ok(other);
assert.ok(other.hasChildren);
let deep = utils.getByName(other, 'deep');
if (isLinux) { // Linux has case sensitive file system
assert.ok(deep);
assert.ok(deep.hasChildren);
assert.ok(!deep.children); // not resolved because we got instructed to resolve other/Deep with capital D
} else {
assert.ok(deep);
assert.ok(deep.hasChildren);
assert.equal(deep.children.length, 4);
}
})
.done(() => done(), done);
});
test('resolve directory - resolveTo multiple directories', function (done: () => void) {
let resolver = create('/');
resolver.resolve({ resolveTo: [toResource('other/deep'), toResource('examples')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.hasChildren);
assert.ok(result.isDirectory);
let children = result.children;
assert.equal(children.length, 4);
let other = utils.getByName(result, 'other');
assert.ok(other);
assert.ok(other.hasChildren);
let deep = utils.getByName(other, 'deep');
assert.ok(deep);
assert.ok(deep.hasChildren);
assert.equal(deep.children.length, 4);
let examples = utils.getByName(result, 'examples');
assert.ok(examples);
assert.ok(examples.hasChildren);
assert.equal(examples.children.length, 4);
})
.done(() => done(), done);
});
test('resolve directory - resolveSingleChildFolders', function (done: () => void) {
let resolver = create('/other');
resolver.resolve({ resolveSingleChildDescendants: true }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.hasChildren);
assert.ok(result.isDirectory);
let children = result.children;
assert.equal(children.length, 1);
let deep = utils.getByName(result, 'deep');
assert.ok(deep);
assert.ok(deep.hasChildren);
assert.equal(deep.children.length, 4);
})
.done(() => done(), done);
});
});

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IFileStat } from 'vs/platform/files/common/files';
export function getByName(root: IFileStat, name: string): IFileStat {
for (let i = 0; i < root.children.length; i++) {
if (root.children[i].name === name) {
return root.children[i];
}
}
return null;
}

View File

@@ -0,0 +1,226 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import assert = require('assert');
import platform = require('vs/base/common/platform');
import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
import uri from 'vs/base/common/uri';
import { IRawFileChange, toFileChangesEvent, normalize } from 'vs/workbench/services/files/node/watcher/common';
import Event, { Emitter } from 'vs/base/common/event';
class TestFileWatcher {
private _onFileChanges: Emitter<FileChangesEvent>;
constructor() {
this._onFileChanges = new Emitter<FileChangesEvent>();
}
public get onFileChanges(): Event<FileChangesEvent> {
return this._onFileChanges.event;
}
public report(changes: IRawFileChange[]): void {
this.onRawFileEvents(changes);
}
private onRawFileEvents(events: IRawFileChange[]): void {
// Normalize
let normalizedEvents = normalize(events);
// Emit through event emitter
if (normalizedEvents.length > 0) {
this._onFileChanges.fire(toFileChangesEvent(normalizedEvents));
}
}
}
enum Path {
UNIX,
WINDOWS,
UNC
};
suite('Watcher', () => {
test('watching - simple add/update/delete', function (done: () => void) {
const watch = new TestFileWatcher();
const added = uri.file('/users/data/src/added.txt');
const updated = uri.file('/users/data/src/updated.txt');
const deleted = uri.file('/users/data/src/deleted.txt');
const raw: IRawFileChange[] = [
{ path: added.fsPath, type: FileChangeType.ADDED },
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: deleted.fsPath, type: FileChangeType.DELETED },
];
watch.onFileChanges(e => {
assert.ok(e);
assert.equal(e.changes.length, 3);
assert.ok(e.contains(added, FileChangeType.ADDED));
assert.ok(e.contains(updated, FileChangeType.UPDATED));
assert.ok(e.contains(deleted, FileChangeType.DELETED));
done();
});
watch.report(raw);
});
let pathSpecs = platform.isWindows ? [Path.WINDOWS, Path.UNC] : [Path.UNIX];
pathSpecs.forEach((p) => {
test('watching - delete only reported for top level folder (' + p + ')', function (done: () => void) {
const watch = new TestFileWatcher();
const deletedFolderA = uri.file(p === Path.UNIX ? '/users/data/src/todelete1' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete1' : '\\\\localhost\\users\\data\\src\\todelete1');
const deletedFolderB = uri.file(p === Path.UNIX ? '/users/data/src/todelete2' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2' : '\\\\localhost\\users\\data\\src\\todelete2');
const deletedFolderBF1 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/file.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\file.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\file.txt');
const deletedFolderBF2 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/more/test.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\more\\test.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\more\\test.txt');
const deletedFolderBF3 = uri.file(p === Path.UNIX ? '/users/data/src/todelete2/super/bar/foo.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\todelete2\\super\\bar\\foo.txt' : '\\\\localhost\\users\\data\\src\\todelete2\\super\\bar\\foo.txt');
const deletedFileA = uri.file(p === Path.UNIX ? '/users/data/src/deleteme.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\deleteme.txt' : '\\\\localhost\\users\\data\\src\\deleteme.txt');
const addedFile = uri.file(p === Path.UNIX ? '/users/data/src/added.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\added.txt' : '\\\\localhost\\users\\data\\src\\added.txt');
const updatedFile = uri.file(p === Path.UNIX ? '/users/data/src/updated.txt' : p === Path.WINDOWS ? 'C:\\users\\data\\src\\updated.txt' : '\\\\localhost\\users\\data\\src\\updated.txt');
const raw: IRawFileChange[] = [
{ path: deletedFolderA.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderB.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderBF1.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderBF2.fsPath, type: FileChangeType.DELETED },
{ path: deletedFolderBF3.fsPath, type: FileChangeType.DELETED },
{ path: deletedFileA.fsPath, type: FileChangeType.DELETED },
{ path: addedFile.fsPath, type: FileChangeType.ADDED },
{ path: updatedFile.fsPath, type: FileChangeType.UPDATED }
];
watch.onFileChanges(e => {
assert.ok(e);
assert.equal(e.changes.length, 5);
assert.ok(e.contains(deletedFolderA, FileChangeType.DELETED));
assert.ok(e.contains(deletedFolderB, FileChangeType.DELETED));
assert.ok(e.contains(deletedFileA, FileChangeType.DELETED));
assert.ok(e.contains(addedFile, FileChangeType.ADDED));
assert.ok(e.contains(updatedFile, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
});
test('watching - event normalization: ignore CREATE followed by DELETE', function (done: () => void) {
const watch = new TestFileWatcher();
const created = uri.file('/users/data/src/related');
const deleted = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IRawFileChange[] = [
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: deleted.fsPath, type: FileChangeType.DELETED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onFileChanges(e => {
assert.ok(e);
assert.equal(e.changes.length, 1);
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
test('watching - event normalization: flatten DELETE followed by CREATE into CHANGE', function (done: () => void) {
const watch = new TestFileWatcher();
const deleted = uri.file('/users/data/src/related');
const created = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IRawFileChange[] = [
{ path: deleted.fsPath, type: FileChangeType.DELETED },
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onFileChanges(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.ok(e.contains(deleted, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
test('watching - event normalization: ignore UPDATE when CREATE received', function (done: () => void) {
const watch = new TestFileWatcher();
const created = uri.file('/users/data/src/related');
const updated = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IRawFileChange[] = [
{ path: created.fsPath, type: FileChangeType.ADDED },
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
];
watch.onFileChanges(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.ok(e.contains(created, FileChangeType.ADDED));
assert.ok(!e.contains(created, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
test('watching - event normalization: apply DELETE', function (done: () => void) {
const watch = new TestFileWatcher();
const updated = uri.file('/users/data/src/related');
const updated2 = uri.file('/users/data/src/related');
const deleted = uri.file('/users/data/src/related');
const unrelated = uri.file('/users/data/src/unrelated');
const raw: IRawFileChange[] = [
{ path: updated.fsPath, type: FileChangeType.UPDATED },
{ path: updated2.fsPath, type: FileChangeType.UPDATED },
{ path: unrelated.fsPath, type: FileChangeType.UPDATED },
{ path: updated.fsPath, type: FileChangeType.DELETED }
];
watch.onFileChanges(e => {
assert.ok(e);
assert.equal(e.changes.length, 2);
assert.ok(e.contains(deleted, FileChangeType.DELETED));
assert.ok(!e.contains(updated, FileChangeType.UPDATED));
assert.ok(e.contains(unrelated, FileChangeType.UPDATED));
done();
});
watch.report(raw);
});
});

View File

@@ -0,0 +1,134 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { Position, IEditorInput } from 'vs/platform/editor/common/editor';
import { IEditorStacksModel, IEditorGroup } from 'vs/workbench/common/editor';
import Event from 'vs/base/common/event';
export enum GroupArrangement {
MINIMIZE_OTHERS,
EVEN
}
export type GroupOrientation = 'vertical' | 'horizontal';
export const IEditorGroupService = createDecorator<IEditorGroupService>('editorGroupService');
export interface ITabOptions {
showTabs?: boolean;
tabCloseButton?: 'left' | 'right' | 'off';
showIcons?: boolean;
previewEditors?: boolean;
}
export interface IMoveOptions {
index?: number;
inactive?: boolean;
preserveFocus?: boolean;
}
/**
* The editor service allows to open editors and work on the active
* editor input and models.
*/
export interface IEditorGroupService {
_serviceBrand: ServiceIdentifier<any>;
/**
* Emitted when editors or inputs change. Examples: opening, closing of editors. Active editor change.
*/
onEditorsChanged: Event<void>;
/**
* Emitted when opening an editor fails.
*/
onEditorOpenFail: Event<IEditorInput>;
/**
* Emitted when a editors are moved to another position.
*/
onEditorsMoved: Event<void>;
/**
* Emitted when the editor group orientation was changed.
*/
onGroupOrientationChanged: Event<void>;
/**
* Emitted when tab options changed.
*/
onTabOptionsChanged: Event<ITabOptions>;
/**
* Keyboard focus the editor group at the provided position.
*/
focusGroup(group: IEditorGroup): void;
focusGroup(position: Position): void;
/**
* Activate the editor group at the provided position without moving focus.
*/
activateGroup(group: IEditorGroup): void;
activateGroup(position: Position): void;
/**
* Allows to move the editor group from one position to another.
*/
moveGroup(from: IEditorGroup, to: IEditorGroup): void;
moveGroup(from: Position, to: Position): void;
/**
* Allows to arrange editor groups according to the GroupArrangement enumeration.
*/
arrangeGroups(arrangement: GroupArrangement): void;
/**
* Changes the editor group layout between vertical and horizontal orientation. Only applies
* if more than one editor is opened.
*/
setGroupOrientation(orientation: GroupOrientation): void;
/**
* Returns the current editor group layout.
*/
getGroupOrientation(): GroupOrientation;
/**
* Resize visible editor groups
*/
resizeGroup(position: Position, groupSizeChange: number): void;
/**
* Adds the pinned state to an editor, removing it from being a preview editor.
*/
pinEditor(group: IEditorGroup, input: IEditorInput): void;
pinEditor(position: Position, input: IEditorInput): void;
/**
* Removes the pinned state of an editor making it a preview editor.
*/
unpinEditor(group: IEditorGroup, input: IEditorInput): void;
unpinEditor(position: Position, input: IEditorInput): void;
/**
* Moves an editor from one group to another. The index in the group is optional.
* The inactive option is applied when moving across groups.
*/
moveEditor(input: IEditorInput, from: IEditorGroup, to: IEditorGroup, moveOptions?: IMoveOptions): void;
moveEditor(input: IEditorInput, from: Position, to: Position, moveOptions?: IMoveOptions): void;
/**
* Provides access to the editor stacks model
*/
getStacksModel(): IEditorStacksModel;
/**
* Returns tab options.
*/
getTabOptions(): ITabOptions;
}

View File

@@ -0,0 +1,708 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import errors = require('vs/base/common/errors');
import URI from 'vs/base/common/uri';
import { IEditor } from 'vs/editor/common/editorCommon';
import { IEditor as IBaseEditor, IEditorInput, ITextEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor';
import { EditorInput, IEditorCloseEvent, IEditorRegistry, Extensions, toResource, IEditorGroup } from 'vs/workbench/common/editor';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { FileChangesEvent, IFileService, FileChangeType } from 'vs/platform/files/common/files';
import { Selection } from 'vs/editor/common/core/selection';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { once } from 'vs/base/common/event';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { getCodeEditor } from 'vs/editor/common/services/codeEditorService';
import { getExcludes, ISearchConfiguration } from 'vs/platform/search/common/search';
import { parse, IExpression } from 'vs/base/common/glob';
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResourceGlobMatcher } from 'vs/workbench/common/resources';
/**
* Stores the selection & view state of an editor and allows to compare it to other selection states.
*/
export class EditorState {
private static EDITOR_SELECTION_THRESHOLD = 5; // number of lines to move in editor to justify for new state
constructor(private _editorInput: IEditorInput, private _selection: Selection) {
}
public get editorInput(): IEditorInput {
return this._editorInput;
}
public get selection(): Selection {
return this._selection;
}
public justifiesNewPushState(other: EditorState, event?: ICursorPositionChangedEvent): boolean {
if (!this._editorInput.matches(other._editorInput)) {
return true; // push different editor inputs
}
if (!Selection.isISelection(this._selection) || !Selection.isISelection(other._selection)) {
return true; // unknown selections
}
if (event && event.source === 'api') {
return true; // always let API source win (e.g. "Go to definition" should add a history entry)
}
const myLineNumber = Math.min(this._selection.selectionStartLineNumber, this._selection.positionLineNumber);
const otherLineNumber = Math.min(other._selection.selectionStartLineNumber, other._selection.positionLineNumber);
if (Math.abs(myLineNumber - otherLineNumber) < EditorState.EDITOR_SELECTION_THRESHOLD) {
return false; // ignore selection changes in the range of EditorState.EDITOR_SELECTION_THRESHOLD lines
}
return true;
}
}
interface ISerializedFileHistoryEntry {
resource?: string;
resourceJSON: object;
}
export abstract class BaseHistoryService {
protected toUnbind: IDisposable[];
private activeEditorListeners: IDisposable[];
constructor(
protected editorGroupService: IEditorGroupService,
protected editorService: IWorkbenchEditorService
) {
this.toUnbind = [];
this.activeEditorListeners = [];
// Listeners
this.toUnbind.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
}
private onEditorsChanged(): void {
// Dispose old listeners
dispose(this.activeEditorListeners);
this.activeEditorListeners = [];
const activeEditor = this.editorService.getActiveEditor();
// Propagate to history
this.handleActiveEditorChange(activeEditor);
// Apply listener for selection changes if this is a text editor
const control = getCodeEditor(activeEditor);
if (control) {
this.activeEditorListeners.push(control.onDidChangeCursorPosition(event => {
this.handleEditorSelectionChangeEvent(activeEditor, event);
}));
}
}
protected abstract handleExcludesChange(): void;
protected abstract handleEditorSelectionChangeEvent(editor?: IBaseEditor, event?: ICursorPositionChangedEvent): void;
protected abstract handleActiveEditorChange(editor?: IBaseEditor): void;
public dispose(): void {
this.toUnbind = dispose(this.toUnbind);
}
}
interface IStackEntry {
input: IEditorInput | IResourceInput;
options?: ITextEditorOptions;
timestamp: number;
}
interface IRecentlyClosedFile {
resource: URI;
index: number;
}
export class HistoryService extends BaseHistoryService implements IHistoryService {
public _serviceBrand: any;
private static STORAGE_KEY = 'history.entries';
private static MAX_HISTORY_ITEMS = 200;
private static MAX_STACK_ITEMS = 20;
private static MAX_RECENTLY_CLOSED_EDITORS = 20;
private static MERGE_EVENT_CHANGES_THRESHOLD = 100;
private stack: IStackEntry[];
private index: number;
private navigatingInStack: boolean;
private currentFileEditorState: EditorState;
private history: (IEditorInput | IResourceInput)[];
private recentlyClosedFiles: IRecentlyClosedFile[];
private loaded: boolean;
private registry: IEditorRegistry;
private resourceFilter: ResourceGlobMatcher;
constructor(
@IWorkbenchEditorService editorService: IWorkbenchEditorService,
@IEditorGroupService editorGroupService: IEditorGroupService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IStorageService private storageService: IStorageService,
@IConfigurationService private configurationService: IConfigurationService,
@ILifecycleService private lifecycleService: ILifecycleService,
@IFileService private fileService: IFileService,
@IWindowsService private windowService: IWindowsService,
@IInstantiationService private instantiationService: IInstantiationService,
) {
super(editorGroupService, editorService);
this.index = -1;
this.stack = [];
this.recentlyClosedFiles = [];
this.loaded = false;
this.registry = Registry.as<IEditorRegistry>(Extensions.Editors);
this.resourceFilter = instantiationService.createInstance(ResourceGlobMatcher, root => this.getExcludes(root), (expression: IExpression) => parse(expression));
this.registerListeners();
}
private getExcludes(root?: URI): IExpression {
const scope = root ? { resource: root } : void 0;
return getExcludes(this.configurationService.getConfiguration<ISearchConfiguration>(void 0, scope));
}
private registerListeners(): void {
this.toUnbind.push(this.lifecycleService.onShutdown(reason => this.save()));
this.toUnbind.push(this.editorGroupService.onEditorOpenFail(editor => this.remove(editor)));
this.toUnbind.push(this.editorGroupService.getStacksModel().onEditorClosed(event => this.onEditorClosed(event)));
this.toUnbind.push(this.fileService.onFileChanges(e => this.onFileChanges(e)));
this.toUnbind.push(this.resourceFilter.onExpressionChange(() => this.handleExcludesChange()));
}
private onFileChanges(e: FileChangesEvent): void {
if (e.gotDeleted()) {
this.remove(e); // remove from history files that got deleted or moved
}
}
private onEditorClosed(event: IEditorCloseEvent): void {
// Track closing of pinned editor to support to reopen closed editors
if (event.pinned) {
const file = toResource(event.editor, { filter: 'file' }); // we only support files to reopen
if (file) {
// Remove all inputs matching and add as last recently closed
this.removeFromRecentlyClosedFiles(event.editor);
this.recentlyClosedFiles.push({ resource: file, index: event.index });
// Bounding
if (this.recentlyClosedFiles.length > HistoryService.MAX_RECENTLY_CLOSED_EDITORS) {
this.recentlyClosedFiles.shift();
}
}
}
}
public reopenLastClosedEditor(): void {
this.ensureHistoryLoaded();
const stacks = this.editorGroupService.getStacksModel();
let lastClosedFile = this.recentlyClosedFiles.pop();
while (lastClosedFile && this.isFileOpened(lastClosedFile.resource, stacks.activeGroup)) {
lastClosedFile = this.recentlyClosedFiles.pop(); // pop until we find a file that is not opened
}
if (lastClosedFile) {
this.editorService.openEditor({ resource: lastClosedFile.resource, options: { pinned: true, index: lastClosedFile.index } });
}
}
public forward(acrossEditors?: boolean): void {
if (this.stack.length > this.index + 1) {
if (acrossEditors) {
this.doForwardAcrossEditors();
} else {
this.doForwardInEditors();
}
}
}
private doForwardInEditors(): void {
this.index++;
this.navigate();
}
private doForwardAcrossEditors(): void {
let currentIndex = this.index;
const currentEntry = this.stack[this.index];
// Find the next entry that does not match our current entry
while (this.stack.length > currentIndex + 1) {
currentIndex++;
const previousEntry = this.stack[currentIndex];
if (!this.matches(currentEntry.input, previousEntry.input)) {
this.index = currentIndex;
this.navigate(true /* across editors */);
break;
}
}
}
public back(acrossEditors?: boolean): void {
if (this.index > 0) {
if (acrossEditors) {
this.doBackAcrossEditors();
} else {
this.doBackInEditors();
}
}
}
private doBackInEditors(): void {
this.index--;
this.navigate();
}
private doBackAcrossEditors(): void {
let currentIndex = this.index;
const currentEntry = this.stack[this.index];
// Find the next previous entry that does not match our current entry
while (currentIndex > 0) {
currentIndex--;
const previousEntry = this.stack[currentIndex];
if (!this.matches(currentEntry.input, previousEntry.input)) {
this.index = currentIndex;
this.navigate(true /* across editors */);
break;
}
}
}
public clear(): void {
this.ensureHistoryLoaded();
this.index = -1;
this.stack.splice(0);
this.history = [];
this.recentlyClosedFiles = [];
}
private navigate(acrossEditors?: boolean): void {
const entry = this.stack[this.index];
let options = entry.options;
if (options && !acrossEditors /* ignore line/col options when going across editors */) {
options.revealIfOpened = true;
} else {
options = { revealIfOpened: true };
}
this.navigatingInStack = true;
let openEditorPromise: TPromise<IBaseEditor>;
if (entry.input instanceof EditorInput) {
openEditorPromise = this.editorService.openEditor(entry.input, options);
} else {
openEditorPromise = this.editorService.openEditor({ resource: (entry.input as IResourceInput).resource, options });
}
openEditorPromise.done(() => {
this.navigatingInStack = false;
}, error => {
this.navigatingInStack = false;
errors.onUnexpectedError(error);
});
}
protected handleEditorSelectionChangeEvent(editor?: IBaseEditor, event?: ICursorPositionChangedEvent): void {
this.handleEditorEventInStack(editor, event);
}
protected handleActiveEditorChange(editor?: IBaseEditor): void {
this.handleEditorEventInHistory(editor);
this.handleEditorEventInStack(editor);
}
private handleEditorEventInHistory(editor?: IBaseEditor): void {
const input = editor ? editor.input : void 0;
// Ensure we have at least a name to show and not configured to exclude input
if (!input || !input.getName() || !this.include(input)) {
return;
}
this.ensureHistoryLoaded();
const historyInput = this.preferResourceInput(input);
// Remove any existing entry and add to the beginning
this.removeFromHistory(input);
this.history.unshift(historyInput);
// Respect max entries setting
if (this.history.length > HistoryService.MAX_HISTORY_ITEMS) {
this.history.pop();
}
// Remove this from the history unless the history input is a resource
// that can easily be restored even when the input gets disposed
if (historyInput instanceof EditorInput) {
const onceDispose = once(historyInput.onDispose);
onceDispose(() => {
this.removeFromHistory(input);
});
}
}
private include(input: IEditorInput | IResourceInput): boolean {
if (input instanceof EditorInput) {
return true; // include any non files
}
const resourceInput = input as IResourceInput;
return !this.resourceFilter.matches(resourceInput.resource);
}
protected handleExcludesChange(): void {
this.removeExcludedFromHistory();
}
public remove(input: IEditorInput | IResourceInput): void;
public remove(input: FileChangesEvent): void;
public remove(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
this.removeFromHistory(arg1);
this.removeFromStack(arg1);
this.removeFromRecentlyClosedFiles(arg1);
this.removeFromRecentlyOpened(arg1);
}
private removeExcludedFromHistory(): void {
this.ensureHistoryLoaded();
this.history = this.history.filter(e => this.include(e));
}
private removeFromHistory(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
this.ensureHistoryLoaded();
this.history = this.history.filter(e => !this.matches(arg1, e));
}
private handleEditorEventInStack(editor: IBaseEditor, event?: ICursorPositionChangedEvent): void {
const control = getCodeEditor(editor);
// treat editor changes that happen as part of stack navigation specially
// we do not want to add a new stack entry as a matter of navigating the
// stack but we need to keep our currentFileEditorState up to date with
// the navigtion that occurs.
if (this.navigatingInStack) {
if (control && editor.input) {
this.currentFileEditorState = new EditorState(editor.input, control.getSelection());
} else {
this.currentFileEditorState = null; // we navigated to a non file editor
}
return;
}
if (control && editor.input) {
this.handleTextEditorEvent(editor, control, event);
return;
}
this.currentFileEditorState = null; // at this time we have no active file editor view state
if (editor && editor.input) {
this.handleNonTextEditorEvent(editor);
}
}
private handleTextEditorEvent(editor: IBaseEditor, editorControl: IEditor, event?: ICursorPositionChangedEvent): void {
const stateCandidate = new EditorState(editor.input, editorControl.getSelection());
if (!this.currentFileEditorState || this.currentFileEditorState.justifiesNewPushState(stateCandidate, event)) {
this.currentFileEditorState = stateCandidate;
let options: ITextEditorOptions;
const selection = editorControl.getSelection();
if (selection) {
options = {
selection: { startLineNumber: selection.startLineNumber, startColumn: selection.startColumn }
};
}
this.add(editor.input, options, true /* from event */);
}
}
private handleNonTextEditorEvent(editor: IBaseEditor): void {
const currentStack = this.stack[this.index];
if (currentStack && this.matches(editor.input, currentStack.input)) {
return; // do not push same editor input again
}
this.add(editor.input, void 0, true /* from event */);
}
public add(input: IEditorInput, options?: ITextEditorOptions, fromEvent?: boolean): void {
if (!this.navigatingInStack) {
this.addToStack(input, options, fromEvent);
}
}
private addToStack(input: IEditorInput, options?: ITextEditorOptions, fromEvent?: boolean): void {
// Overwrite an entry in the stack if we have a matching input that comes
// with editor options to indicate that this entry is more specific. Also
// prevent entries that have the exact same options. Finally, Overwrite
// entries if it came from an event and we detect that the change came in
// very fast which indicates that it was not coming in from a user change
// but rather rapid programmatic changes. We just take the last of the changes
// to not cause too many entries on the stack.
let replace = false;
if (this.stack[this.index]) {
const currentEntry = this.stack[this.index];
if (this.matches(input, currentEntry.input) && (this.sameOptions(currentEntry.options, options) || (fromEvent && Date.now() - currentEntry.timestamp < HistoryService.MERGE_EVENT_CHANGES_THRESHOLD))) {
replace = true;
}
}
const stackInput = this.preferResourceInput(input);
const entry = { input: stackInput, options, timestamp: fromEvent ? Date.now() : void 0 };
// If we are not at the end of history, we remove anything after
if (this.stack.length > this.index + 1) {
this.stack = this.stack.slice(0, this.index + 1);
}
// Replace at current position
if (replace) {
this.stack[this.index] = entry;
}
// Add to stack at current position
else {
this.index++;
this.stack.splice(this.index, 0, entry);
// Check for limit
if (this.stack.length > HistoryService.MAX_STACK_ITEMS) {
this.stack.shift(); // remove first and dispose
if (this.index > 0) {
this.index--;
}
}
}
// Remove this from the stack unless the stack input is a resource
// that can easily be restored even when the input gets disposed
if (stackInput instanceof EditorInput) {
const onceDispose = once(stackInput.onDispose);
onceDispose(() => {
this.removeFromStack(input);
});
}
}
private preferResourceInput(input: IEditorInput): IEditorInput | IResourceInput {
const file = toResource(input, { filter: 'file' });
if (file) {
return { resource: file };
}
return input;
}
private sameOptions(optionsA?: ITextEditorOptions, optionsB?: ITextEditorOptions): boolean {
if (!optionsA && !optionsB) {
return true;
}
if ((!optionsA && optionsB) || (optionsA && !optionsB)) {
return false;
}
const s1 = optionsA.selection;
const s2 = optionsB.selection;
if (!s1 && !s2) {
return true;
}
if ((!s1 && s2) || (s1 && !s2)) {
return false;
}
return s1.startLineNumber === s2.startLineNumber; // we consider the history entry same if we are on the same line
}
private removeFromStack(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
this.stack = this.stack.filter(e => !this.matches(arg1, e.input));
this.index = this.stack.length - 1; // reset index
}
private removeFromRecentlyClosedFiles(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
this.recentlyClosedFiles = this.recentlyClosedFiles.filter(e => !this.matchesFile(e.resource, arg1));
}
private removeFromRecentlyOpened(arg1: IEditorInput | IResourceInput | FileChangesEvent): void {
if (arg1 instanceof EditorInput || arg1 instanceof FileChangesEvent) {
return; // for now do not delete from file events since recently open are likely out of workspace files for which there are no delete events
}
const input = arg1 as IResourceInput;
this.windowService.removeFromRecentlyOpened([input.resource.fsPath]);
}
private isFileOpened(resource: URI, group: IEditorGroup): boolean {
if (!group) {
return false;
}
if (!group.contains(resource)) {
return false; // fast check
}
return group.getEditors().some(e => this.matchesFile(resource, e));
}
private matches(arg1: IEditorInput | IResourceInput | FileChangesEvent, inputB: IEditorInput | IResourceInput): boolean {
if (arg1 instanceof FileChangesEvent) {
if (inputB instanceof EditorInput) {
return false; // we only support this for IResourceInput
}
const resourceInputB = inputB as IResourceInput;
return arg1.contains(resourceInputB.resource, FileChangeType.DELETED);
}
if (arg1 instanceof EditorInput && inputB instanceof EditorInput) {
return arg1.matches(inputB);
}
if (arg1 instanceof EditorInput) {
return this.matchesFile((inputB as IResourceInput).resource, arg1);
}
if (inputB instanceof EditorInput) {
return this.matchesFile((arg1 as IResourceInput).resource, inputB);
}
const resourceInputA = arg1 as IResourceInput;
const resourceInputB = inputB as IResourceInput;
return resourceInputA && resourceInputB && resourceInputA.resource.toString() === resourceInputB.resource.toString();
}
private matchesFile(resource: URI, arg2: IEditorInput | IResourceInput | FileChangesEvent): boolean {
if (arg2 instanceof FileChangesEvent) {
return arg2.contains(resource, FileChangeType.DELETED);
}
if (arg2 instanceof EditorInput) {
const file = toResource(arg2, { filter: 'file' });
return file && file.toString() === resource.toString();
}
const resourceInput = arg2 as IResourceInput;
return resourceInput && resourceInput.resource.toString() === resource.toString();
}
public getHistory(): (IEditorInput | IResourceInput)[] {
this.ensureHistoryLoaded();
return this.history.slice(0);
}
private ensureHistoryLoaded(): void {
if (!this.loaded) {
this.loadHistory();
}
this.loaded = true;
}
private save(): void {
if (!this.history) {
return; // nothing to save because history was not used
}
const entries: ISerializedFileHistoryEntry[] = this.history.map(input => {
if (input instanceof EditorInput) {
return void 0; // only file resource inputs are serializable currently
}
return { resourceJSON: (input as IResourceInput).resource.toJSON() };
}).filter(serialized => !!serialized);
this.storageService.store(HistoryService.STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE);
}
private loadHistory(): void {
let entries: ISerializedFileHistoryEntry[] = [];
const entriesRaw = this.storageService.get(HistoryService.STORAGE_KEY, StorageScope.WORKSPACE);
if (entriesRaw) {
entries = JSON.parse(entriesRaw);
}
this.history = entries.map(entry => {
const serializedFileInput = entry as ISerializedFileHistoryEntry;
if (serializedFileInput.resource || serializedFileInput.resourceJSON) {
return { resource: !!serializedFileInput.resourceJSON ? URI.revive(serializedFileInput.resourceJSON) : URI.parse(serializedFileInput.resource) } as IResourceInput;
}
return void 0;
}).filter(input => !!input);
}
public getLastActiveWorkspaceRoot(): URI {
if (!this.contextService.hasWorkspace()) {
return void 0;
}
const history = this.getHistory();
for (let i = 0; i < history.length; i++) {
const input = history[i];
if (input instanceof EditorInput) {
continue;
}
const resourceInput = input as IResourceInput;
const resourceWorkspace = this.contextService.getRoot(resourceInput.resource);
if (resourceWorkspace) {
return resourceWorkspace;
}
}
// fallback to first workspace
return this.contextService.getWorkspace().roots[0];
}
}

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { IEditorInput, ITextEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor';
import URI from 'vs/base/common/uri';
export const IHistoryService = createDecorator<IHistoryService>('historyService');
export interface IHistoryService {
_serviceBrand: ServiceIdentifier<any>;
/**
* Re-opens the last closed editor if any.
*/
reopenLastClosedEditor(): void;
/**
* Add an entry to the navigation stack of the history.
*/
add(input: IEditorInput, options?: ITextEditorOptions): void;
/**
* Navigate forwards in history.
*
* @param acrossEditors instructs the history to skip navigation entries that
* are only within the same document.
*/
forward(acrossEditors?: boolean): void;
/**
* Navigate backwards in history.
*
* @param acrossEditors instructs the history to skip navigation entries that
* are only within the same document.
*/
back(acrossEditors?: boolean): void;
/**
* Removes an entry from history.
*/
remove(input: IEditorInput | IResourceInput): void;
/**
* Clears all history.
*/
clear(): void;
/**
* Get the entire history of opened editors.
*/
getHistory(): (IEditorInput | IResourceInput)[];
/**
* Looking at the editor history, returns the workspace root of the last file that was
* inside the workspace and part of the editor history.
*/
getLastActiveWorkspaceRoot(): URI;
}

View File

@@ -0,0 +1,265 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { isArray } from 'vs/base/common/types';
import { Queue } from 'vs/base/common/async';
import { IReference, Disposable } from 'vs/base/common/lifecycle';
import * as json from 'vs/base/common/json';
import { Edit } from 'vs/base/common/jsonFormatter';
import { setProperty } from 'vs/base/common/jsonEdit';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService } from 'vs/platform/files/common/files';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
export const IKeybindingEditingService = createDecorator<IKeybindingEditingService>('keybindingEditingService');
export interface IKeybindingEditingService {
_serviceBrand: ServiceIdentifier<any>;
editKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): TPromise<void>;
removeKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void>;
resetKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void>;
}
export class KeybindingsEditingService extends Disposable implements IKeybindingEditingService {
public _serviceBrand: any;
private queue: Queue<void>;
private resource: URI = URI.file(this.environmentService.appKeybindingsPath);
constructor(
@ITextModelService private textModelResolverService: ITextModelService,
@ITextFileService private textFileService: ITextFileService,
@IFileService private fileService: IFileService,
@IConfigurationService private configurationService: IConfigurationService,
@IEnvironmentService private environmentService: IEnvironmentService
) {
super();
this.queue = new Queue<void>();
}
editKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): TPromise<void> {
return this.queue.queue(() => this.doEditKeybinding(key, keybindingItem)); // queue up writes to prevent race conditions
}
resetKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void> {
return this.queue.queue(() => this.doResetKeybinding(keybindingItem)); // queue up writes to prevent race conditions
}
removeKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void> {
return this.queue.queue(() => this.doRemoveKeybinding(keybindingItem)); // queue up writes to prevent race conditions
}
private doEditKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): TPromise<void> {
return this.resolveAndValidate()
.then(reference => {
const model = reference.object.textEditorModel;
if (keybindingItem.isDefault) {
this.updateDefaultKeybinding(key, keybindingItem, model);
} else {
this.updateUserKeybinding(key, keybindingItem, model);
}
return this.save().then(() => reference.dispose());
});
}
private doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void> {
return this.resolveAndValidate()
.then(reference => {
const model = reference.object.textEditorModel;
if (keybindingItem.isDefault) {
this.removeDefaultKeybinding(keybindingItem, model);
} else {
this.removeUserKeybinding(keybindingItem, model);
}
return this.save().then(() => reference.dispose());
});
}
private doResetKeybinding(keybindingItem: ResolvedKeybindingItem): TPromise<void> {
return this.resolveAndValidate()
.then(reference => {
const model = reference.object.textEditorModel;
if (!keybindingItem.isDefault) {
this.removeUserKeybinding(keybindingItem, model);
this.removeUnassignedDefaultKeybinding(keybindingItem, model);
}
return this.save().then(() => reference.dispose());
});
}
private save(): TPromise<any> {
return this.textFileService.save(this.resource);
}
private updateUserKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
if (userKeybindingEntryIndex !== -1) {
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);
}
}
private updateDefaultKeybinding(newKey: string, keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
if (userKeybindingEntryIndex !== -1) {
// Update the keybinding with new key
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex, 'key'], newKey, { tabSize, insertSpaces, eol })[0], model);
} else {
// Add the new keybinidng with new key
this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(newKey, keybindingItem.command, keybindingItem.when, false), { tabSize, insertSpaces, eol })[0], model);
}
if (keybindingItem.resolvedKeybinding) {
// Unassign the default keybinding
this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.resolvedKeybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when, true), { tabSize, insertSpaces, eol })[0], model);
}
}
private removeUserKeybinding(keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
if (userKeybindingEntryIndex !== -1) {
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex], void 0, { tabSize, insertSpaces, eol })[0], model);
}
}
private removeDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
this.applyEditsToBuffer(setProperty(model.getValue(), [-1], this.asObject(keybindingItem.resolvedKeybinding.getUserSettingsLabel(), keybindingItem.command, keybindingItem.when, true), { tabSize, insertSpaces, eol })[0], model);
}
private removeUnassignedDefaultKeybinding(keybindingItem: ResolvedKeybindingItem, model: editorCommon.IModel): void {
const { tabSize, insertSpaces } = model.getOptions();
const eol = model.getEOL();
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
const index = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
if (index !== -1) {
this.applyEditsToBuffer(setProperty(model.getValue(), [index], void 0, { tabSize, insertSpaces, eol })[0], model);
}
}
private findUserKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number {
for (let index = 0; index < userKeybindingEntries.length; index++) {
const keybinding = userKeybindingEntries[index];
if (keybinding.command === keybindingItem.command) {
if (!keybinding.when && !keybindingItem.when) {
return index;
}
if (keybinding.when && keybindingItem.when) {
if (ContextKeyExpr.deserialize(keybinding.when).serialize() === keybindingItem.when.serialize()) {
return index;
}
}
}
}
return -1;
}
private findUnassignedDefaultKeybindingEntryIndex(keybindingItem: ResolvedKeybindingItem, userKeybindingEntries: IUserFriendlyKeybinding[]): number {
for (let index = 0; index < userKeybindingEntries.length; index++) {
if (userKeybindingEntries[index].command === `-${keybindingItem.command}`) {
return index;
}
}
return -1;
}
private asObject(key: string, command: string, when: ContextKeyExpr, negate: boolean): any {
const object = { key };
object['command'] = negate ? `-${command}` : command;
if (when) {
object['when'] = when.serialize();
}
return object;
}
private applyEditsToBuffer(edit: Edit, model: editorCommon.IModel): void {
const startPosition = model.getPositionAt(edit.offset);
const endPosition = model.getPositionAt(edit.offset + edit.length);
const range = new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column);
let currentText = model.getValueInRange(range);
const editOperation = currentText ? EditOperation.replace(range, edit.content) : EditOperation.insert(startPosition, edit.content);
model.pushEditOperations([new Selection(startPosition.lineNumber, startPosition.column, startPosition.lineNumber, startPosition.column)], [editOperation], () => []);
}
private resolveModelReference(): TPromise<IReference<ITextEditorModel>> {
return this.fileService.existsFile(this.resource)
.then(exists => {
const EOL = this.configurationService.getConfiguration('files', { overrideIdentifier: 'json' })['eol'];
const result = exists ? TPromise.as(null) : this.fileService.updateContent(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' });
return result.then(() => this.textModelResolverService.createModelReference(this.resource));
});
}
private resolveAndValidate(): TPromise<IReference<ITextEditorModel>> {
// Target cannot be dirty if not writing into buffer
if (this.textFileService.isDirty(this.resource)) {
return TPromise.wrapError<IReference<ITextEditorModel>>(new Error(localize('errorKeybindingsFileDirty', "Unable to write because the file is dirty. Please save the **Keybindings** file and try again.")));
}
return this.resolveModelReference()
.then(reference => {
const model = reference.object.textEditorModel;
const EOL = model.getEOL();
if (model.getValue()) {
const parsed = this.parse(model);
if (parsed.parseErrors.length) {
return TPromise.wrapError<IReference<ITextEditorModel>>(new Error(localize('parseErrors', "Unable to write keybindings. Please open **Keybindings file** to correct errors/warnings in the file and try again.")));
}
if (parsed.result) {
if (!isArray(parsed.result)) {
return TPromise.wrapError<IReference<ITextEditorModel>>(new Error(localize('errorInvalidConfiguration', "Unable to write keybindings. **Keybindings file** has an object which is not of type Array. Please open the file to clean up and try again.")));
}
} else {
const content = EOL + '[]';
this.applyEditsToBuffer({ content, length: content.length, offset: model.getValue().length }, model);
}
} else {
const content = this.getEmptyContent(EOL);
this.applyEditsToBuffer({ content, length: content.length, offset: 0 }, model);
}
return reference;
});
}
private parse(model: editorCommon.IModel): { result: IUserFriendlyKeybinding[], parseErrors: json.ParseError[] } {
const parseErrors: json.ParseError[] = [];
const result = json.parse(model.getValue(), parseErrors, { allowTrailingComma: true });
return { result, parseErrors };
}
private getEmptyContent(EOL: string): string {
return '// ' + localize('emptyKeybindingsHeader', "Place your key bindings in this file to overwrite the defaults") + EOL + '[]';
}
}

View File

@@ -0,0 +1,194 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Keybinding, SimpleKeybinding, ChordKeybinding, KeyCodeUtils } from 'vs/base/common/keyCodes';
import { OperatingSystem } from 'vs/base/common/platform';
import { IUserFriendlyKeybinding } from 'vs/platform/keybinding/common/keybinding';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { ScanCodeBinding, ScanCodeUtils } from 'vs/workbench/services/keybinding/common/scanCode';
export interface IUserKeybindingItem {
firstPart: SimpleKeybinding | ScanCodeBinding;
chordPart: SimpleKeybinding | ScanCodeBinding;
command: string;
commandArgs?: any;
when: ContextKeyExpr;
}
export class KeybindingIO {
public static writeKeybindingItem(out: OutputBuilder, item: ResolvedKeybindingItem, OS: OperatingSystem): void {
let quotedSerializedKeybinding = JSON.stringify(item.resolvedKeybinding.getUserSettingsLabel());
out.write(`{ "key": ${rightPaddedString(quotedSerializedKeybinding + ',', 25)} "command": `);
let serializedWhen = item.when ? item.when.serialize() : '';
let quotedSerializeCommand = JSON.stringify(item.command);
if (serializedWhen.length > 0) {
out.write(`${quotedSerializeCommand},`);
out.writeLine();
out.write(` "when": "${serializedWhen}" `);
} else {
out.write(`${quotedSerializeCommand} `);
}
// out.write(String(item.weight1 + '-' + item.weight2));
out.write('}');
}
public static readUserKeybindingItem(input: IUserFriendlyKeybinding, OS: OperatingSystem): IUserKeybindingItem {
const [firstPart, chordPart] = (typeof input.key === 'string' ? this._readUserBinding(input.key) : [null, null]);
const when = (typeof input.when === 'string' ? ContextKeyExpr.deserialize(input.when) : null);
const command = (typeof input.command === 'string' ? input.command : null);
const commandArgs = (typeof input.args !== 'undefined' ? input.args : null);
return {
firstPart: firstPart,
chordPart: chordPart,
command: command,
commandArgs: commandArgs,
when: when
};
}
private static _readModifiers(input: string) {
input = input.toLowerCase().trim();
let ctrl = false;
let shift = false;
let alt = false;
let meta = false;
let matchedModifier: boolean;
do {
matchedModifier = false;
if (/^ctrl(\+|\-)/.test(input)) {
ctrl = true;
input = input.substr('ctrl-'.length);
matchedModifier = true;
}
if (/^shift(\+|\-)/.test(input)) {
shift = true;
input = input.substr('shift-'.length);
matchedModifier = true;
}
if (/^alt(\+|\-)/.test(input)) {
alt = true;
input = input.substr('alt-'.length);
matchedModifier = true;
}
if (/^meta(\+|\-)/.test(input)) {
meta = true;
input = input.substr('meta-'.length);
matchedModifier = true;
}
if (/^win(\+|\-)/.test(input)) {
meta = true;
input = input.substr('win-'.length);
matchedModifier = true;
}
if (/^cmd(\+|\-)/.test(input)) {
meta = true;
input = input.substr('cmd-'.length);
matchedModifier = true;
}
} while (matchedModifier);
let key: string;
const firstSpaceIdx = input.indexOf(' ');
if (firstSpaceIdx > 0) {
key = input.substring(0, firstSpaceIdx);
input = input.substring(firstSpaceIdx);
} else {
key = input;
input = '';
}
return {
remains: input,
ctrl,
shift,
alt,
meta,
key
};
}
private static _readSimpleKeybinding(input: string): [SimpleKeybinding, string] {
const mods = this._readModifiers(input);
const keyCode = KeyCodeUtils.fromUserSettings(mods.key);
return [new SimpleKeybinding(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains];
}
public static readKeybinding(input: string, OS: OperatingSystem): Keybinding {
if (!input) {
return null;
}
let [firstPart, remains] = this._readSimpleKeybinding(input);
let chordPart: SimpleKeybinding = null;
if (remains.length > 0) {
[chordPart] = this._readSimpleKeybinding(remains);
}
if (chordPart) {
return new ChordKeybinding(firstPart, chordPart);
}
return firstPart;
}
private static _readSimpleUserBinding(input: string): [SimpleKeybinding | ScanCodeBinding, string] {
const mods = this._readModifiers(input);
const scanCodeMatch = mods.key.match(/^\[([^\]]+)\]$/);
if (scanCodeMatch) {
const strScanCode = scanCodeMatch[1];
const scanCode = ScanCodeUtils.lowerCaseToEnum(strScanCode);
return [new ScanCodeBinding(mods.ctrl, mods.shift, mods.alt, mods.meta, scanCode), mods.remains];
}
const keyCode = KeyCodeUtils.fromUserSettings(mods.key);
return [new SimpleKeybinding(mods.ctrl, mods.shift, mods.alt, mods.meta, keyCode), mods.remains];
}
static _readUserBinding(input: string): [SimpleKeybinding | ScanCodeBinding, SimpleKeybinding | ScanCodeBinding] {
if (!input) {
return [null, null];
}
let [firstPart, remains] = this._readSimpleUserBinding(input);
let chordPart: SimpleKeybinding | ScanCodeBinding = null;
if (remains.length > 0) {
[chordPart] = this._readSimpleUserBinding(remains);
}
return [firstPart, chordPart];
}
}
function rightPaddedString(str: string, minChars: number): string {
if (str.length < minChars) {
return str + (new Array(minChars - str.length).join(' '));
}
return str;
}
export class OutputBuilder {
private _lines: string[] = [];
private _currentLine: string = '';
write(str: string): void {
this._currentLine += str;
}
writeLine(str: string = ''): void {
this._lines.push(this._currentLine + str);
this._currentLine = '';
}
toString(): string {
this.writeLine();
return this._lines.join('\n');
}
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode';
export interface IKeyboardMapper {
dumpDebugInfo(): string;
resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
resolveUserBinding(firstPart: SimpleKeybinding | ScanCodeBinding, chordPart: SimpleKeybinding | ScanCodeBinding): ResolvedKeybinding[];
}

View File

@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { OperatingSystem } from 'vs/base/common/platform';
import { ResolvedKeybinding, SimpleKeybinding, Keybinding, KeyCode, ChordKeybinding } from 'vs/base/common/keyCodes';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { ScanCodeBinding, ScanCode, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/workbench/services/keybinding/common/scanCode';
export interface IMacLinuxKeyMapping {
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
valueIsDeadKey?: boolean;
withShiftIsDeadKey?: boolean;
withAltGrIsDeadKey?: boolean;
withShiftAltGrIsDeadKey?: boolean;
}
export interface IMacLinuxKeyboardMapping {
[scanCode: string]: IMacLinuxKeyMapping;
}
/**
* A keyboard mapper to be used when reading the keymap from the OS fails.
*/
export class MacLinuxFallbackKeyboardMapper implements IKeyboardMapper {
/**
* OS (can be Linux or Macintosh)
*/
private readonly _OS: OperatingSystem;
constructor(OS: OperatingSystem) {
this._OS = OS;
}
public dumpDebugInfo(): string {
return 'FallbackKeyboardMapper dispatching on keyCode';
}
public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] {
return [new USLayoutResolvedKeybinding(keybinding, this._OS)];
}
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
let keybinding = new SimpleKeybinding(
keyboardEvent.ctrlKey,
keyboardEvent.shiftKey,
keyboardEvent.altKey,
keyboardEvent.metaKey,
keyboardEvent.keyCode
);
return new USLayoutResolvedKeybinding(keybinding, this._OS);
}
private _scanCodeToKeyCode(scanCode: ScanCode): KeyCode {
const immutableKeyCode = IMMUTABLE_CODE_TO_KEY_CODE[scanCode];
if (immutableKeyCode !== -1) {
return immutableKeyCode;
}
switch (scanCode) {
case ScanCode.KeyA: return KeyCode.KEY_A;
case ScanCode.KeyB: return KeyCode.KEY_B;
case ScanCode.KeyC: return KeyCode.KEY_C;
case ScanCode.KeyD: return KeyCode.KEY_D;
case ScanCode.KeyE: return KeyCode.KEY_E;
case ScanCode.KeyF: return KeyCode.KEY_F;
case ScanCode.KeyG: return KeyCode.KEY_G;
case ScanCode.KeyH: return KeyCode.KEY_H;
case ScanCode.KeyI: return KeyCode.KEY_I;
case ScanCode.KeyJ: return KeyCode.KEY_J;
case ScanCode.KeyK: return KeyCode.KEY_K;
case ScanCode.KeyL: return KeyCode.KEY_L;
case ScanCode.KeyM: return KeyCode.KEY_M;
case ScanCode.KeyN: return KeyCode.KEY_N;
case ScanCode.KeyO: return KeyCode.KEY_O;
case ScanCode.KeyP: return KeyCode.KEY_P;
case ScanCode.KeyQ: return KeyCode.KEY_Q;
case ScanCode.KeyR: return KeyCode.KEY_R;
case ScanCode.KeyS: return KeyCode.KEY_S;
case ScanCode.KeyT: return KeyCode.KEY_T;
case ScanCode.KeyU: return KeyCode.KEY_U;
case ScanCode.KeyV: return KeyCode.KEY_V;
case ScanCode.KeyW: return KeyCode.KEY_W;
case ScanCode.KeyX: return KeyCode.KEY_X;
case ScanCode.KeyY: return KeyCode.KEY_Y;
case ScanCode.KeyZ: return KeyCode.KEY_Z;
case ScanCode.Digit1: return KeyCode.KEY_1;
case ScanCode.Digit2: return KeyCode.KEY_2;
case ScanCode.Digit3: return KeyCode.KEY_3;
case ScanCode.Digit4: return KeyCode.KEY_4;
case ScanCode.Digit5: return KeyCode.KEY_5;
case ScanCode.Digit6: return KeyCode.KEY_6;
case ScanCode.Digit7: return KeyCode.KEY_7;
case ScanCode.Digit8: return KeyCode.KEY_8;
case ScanCode.Digit9: return KeyCode.KEY_9;
case ScanCode.Digit0: return KeyCode.KEY_0;
case ScanCode.Minus: return KeyCode.US_MINUS;
case ScanCode.Equal: return KeyCode.US_EQUAL;
case ScanCode.BracketLeft: return KeyCode.US_OPEN_SQUARE_BRACKET;
case ScanCode.BracketRight: return KeyCode.US_CLOSE_SQUARE_BRACKET;
case ScanCode.Backslash: return KeyCode.US_BACKSLASH;
case ScanCode.IntlHash: return KeyCode.Unknown; // missing
case ScanCode.Semicolon: return KeyCode.US_SEMICOLON;
case ScanCode.Quote: return KeyCode.US_QUOTE;
case ScanCode.Backquote: return KeyCode.US_BACKTICK;
case ScanCode.Comma: return KeyCode.US_COMMA;
case ScanCode.Period: return KeyCode.US_DOT;
case ScanCode.Slash: return KeyCode.US_SLASH;
case ScanCode.IntlBackslash: return KeyCode.OEM_102;
}
return KeyCode.Unknown;
}
private _resolveSimpleUserBinding(binding: SimpleKeybinding | ScanCodeBinding): SimpleKeybinding {
if (!binding) {
return null;
}
if (binding instanceof SimpleKeybinding) {
return binding;
}
const keyCode = this._scanCodeToKeyCode(binding.scanCode);
if (keyCode === KeyCode.Unknown) {
return null;
}
return new SimpleKeybinding(binding.ctrlKey, binding.shiftKey, binding.altKey, binding.metaKey, keyCode);
}
public resolveUserBinding(firstPart: SimpleKeybinding | ScanCodeBinding, chordPart: SimpleKeybinding | ScanCodeBinding): ResolvedKeybinding[] {
const _firstPart = this._resolveSimpleUserBinding(firstPart);
const _chordPart = this._resolveSimpleUserBinding(chordPart);
if (_firstPart && _chordPart) {
return [new USLayoutResolvedKeybinding(new ChordKeybinding(_firstPart, _chordPart), this._OS)];
}
if (_firstPart) {
return [new USLayoutResolvedKeybinding(_firstPart, this._OS)];
}
return [];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,691 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { KeyCode } from 'vs/base/common/keyCodes';
/**
* keyboardEvent.code
*/
export const enum ScanCode {
None,
Hyper,
Super,
Fn,
FnLock,
Suspend,
Resume,
Turbo,
Sleep,
WakeUp,
KeyA,
KeyB,
KeyC,
KeyD,
KeyE,
KeyF,
KeyG,
KeyH,
KeyI,
KeyJ,
KeyK,
KeyL,
KeyM,
KeyN,
KeyO,
KeyP,
KeyQ,
KeyR,
KeyS,
KeyT,
KeyU,
KeyV,
KeyW,
KeyX,
KeyY,
KeyZ,
Digit1,
Digit2,
Digit3,
Digit4,
Digit5,
Digit6,
Digit7,
Digit8,
Digit9,
Digit0,
Enter,
Escape,
Backspace,
Tab,
Space,
Minus,
Equal,
BracketLeft,
BracketRight,
Backslash,
IntlHash,
Semicolon,
Quote,
Backquote,
Comma,
Period,
Slash,
CapsLock,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
PrintScreen,
ScrollLock,
Pause,
Insert,
Home,
PageUp,
Delete,
End,
PageDown,
ArrowRight,
ArrowLeft,
ArrowDown,
ArrowUp,
NumLock,
NumpadDivide,
NumpadMultiply,
NumpadSubtract,
NumpadAdd,
NumpadEnter,
Numpad1,
Numpad2,
Numpad3,
Numpad4,
Numpad5,
Numpad6,
Numpad7,
Numpad8,
Numpad9,
Numpad0,
NumpadDecimal,
IntlBackslash,
ContextMenu,
Power,
NumpadEqual,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
Open,
Help,
Select,
Again,
Undo,
Cut,
Copy,
Paste,
Find,
AudioVolumeMute,
AudioVolumeUp,
AudioVolumeDown,
NumpadComma,
IntlRo,
KanaMode,
IntlYen,
Convert,
NonConvert,
Lang1,
Lang2,
Lang3,
Lang4,
Lang5,
Abort,
Props,
NumpadParenLeft,
NumpadParenRight,
NumpadBackspace,
NumpadMemoryStore,
NumpadMemoryRecall,
NumpadMemoryClear,
NumpadMemoryAdd,
NumpadMemorySubtract,
NumpadClear,
NumpadClearEntry,
ControlLeft,
ShiftLeft,
AltLeft,
MetaLeft,
ControlRight,
ShiftRight,
AltRight,
MetaRight,
BrightnessUp,
BrightnessDown,
MediaPlay,
MediaRecord,
MediaFastForward,
MediaRewind,
MediaTrackNext,
MediaTrackPrevious,
MediaStop,
Eject,
MediaPlayPause,
MediaSelect,
LaunchMail,
LaunchApp2,
LaunchApp1,
SelectTask,
LaunchScreenSaver,
BrowserSearch,
BrowserHome,
BrowserBack,
BrowserForward,
BrowserStop,
BrowserRefresh,
BrowserFavorites,
ZoomToggle,
MailReply,
MailForward,
MailSend,
MAX_VALUE
}
const scanCodeIntToStr: string[] = [];
const scanCodeStrToInt: { [code: string]: number; } = Object.create(null);
const scanCodeLowerCaseStrToInt: { [code: string]: number; } = Object.create(null);
export const ScanCodeUtils = {
lowerCaseToEnum: (scanCode: string) => scanCodeLowerCaseStrToInt[scanCode] || ScanCode.None,
toEnum: (scanCode: string) => scanCodeStrToInt[scanCode] || ScanCode.None,
toString: (scanCode: ScanCode) => scanCodeIntToStr[scanCode] || 'None'
};
/**
* -1 if a ScanCode => KeyCode mapping depends on kb layout.
*/
export const IMMUTABLE_CODE_TO_KEY_CODE: KeyCode[] = [];
/**
* -1 if a KeyCode => ScanCode mapping depends on kb layout.
*/
export const IMMUTABLE_KEY_CODE_TO_CODE: ScanCode[] = [];
export class ScanCodeBinding {
public readonly ctrlKey: boolean;
public readonly shiftKey: boolean;
public readonly altKey: boolean;
public readonly metaKey: boolean;
public readonly scanCode: ScanCode;
constructor(ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, scanCode: ScanCode) {
this.ctrlKey = ctrlKey;
this.shiftKey = shiftKey;
this.altKey = altKey;
this.metaKey = metaKey;
this.scanCode = scanCode;
}
public equals(other: ScanCodeBinding): boolean {
return (
this.ctrlKey === other.ctrlKey
&& this.shiftKey === other.shiftKey
&& this.altKey === other.altKey
&& this.metaKey === other.metaKey
&& this.scanCode === other.scanCode
);
}
/**
* Does this keybinding refer to the key code of a modifier and it also has the modifier flag?
*/
public isDuplicateModifierCase(): boolean {
return (
(this.ctrlKey && (this.scanCode === ScanCode.ControlLeft || this.scanCode === ScanCode.ControlRight))
|| (this.shiftKey && (this.scanCode === ScanCode.ShiftLeft || this.scanCode === ScanCode.ShiftRight))
|| (this.altKey && (this.scanCode === ScanCode.AltLeft || this.scanCode === ScanCode.AltRight))
|| (this.metaKey && (this.scanCode === ScanCode.MetaLeft || this.scanCode === ScanCode.MetaRight))
);
}
}
(function () {
function d(intScanCode: ScanCode, strScanCode: string): void {
scanCodeIntToStr[intScanCode] = strScanCode;
scanCodeStrToInt[strScanCode] = intScanCode;
scanCodeLowerCaseStrToInt[strScanCode.toLowerCase()] = intScanCode;
}
d(ScanCode.None, 'None');
d(ScanCode.Hyper, 'Hyper');
d(ScanCode.Super, 'Super');
d(ScanCode.Fn, 'Fn');
d(ScanCode.FnLock, 'FnLock');
d(ScanCode.Suspend, 'Suspend');
d(ScanCode.Resume, 'Resume');
d(ScanCode.Turbo, 'Turbo');
d(ScanCode.Sleep, 'Sleep');
d(ScanCode.WakeUp, 'WakeUp');
d(ScanCode.KeyA, 'KeyA');
d(ScanCode.KeyB, 'KeyB');
d(ScanCode.KeyC, 'KeyC');
d(ScanCode.KeyD, 'KeyD');
d(ScanCode.KeyE, 'KeyE');
d(ScanCode.KeyF, 'KeyF');
d(ScanCode.KeyG, 'KeyG');
d(ScanCode.KeyH, 'KeyH');
d(ScanCode.KeyI, 'KeyI');
d(ScanCode.KeyJ, 'KeyJ');
d(ScanCode.KeyK, 'KeyK');
d(ScanCode.KeyL, 'KeyL');
d(ScanCode.KeyM, 'KeyM');
d(ScanCode.KeyN, 'KeyN');
d(ScanCode.KeyO, 'KeyO');
d(ScanCode.KeyP, 'KeyP');
d(ScanCode.KeyQ, 'KeyQ');
d(ScanCode.KeyR, 'KeyR');
d(ScanCode.KeyS, 'KeyS');
d(ScanCode.KeyT, 'KeyT');
d(ScanCode.KeyU, 'KeyU');
d(ScanCode.KeyV, 'KeyV');
d(ScanCode.KeyW, 'KeyW');
d(ScanCode.KeyX, 'KeyX');
d(ScanCode.KeyY, 'KeyY');
d(ScanCode.KeyZ, 'KeyZ');
d(ScanCode.Digit1, 'Digit1');
d(ScanCode.Digit2, 'Digit2');
d(ScanCode.Digit3, 'Digit3');
d(ScanCode.Digit4, 'Digit4');
d(ScanCode.Digit5, 'Digit5');
d(ScanCode.Digit6, 'Digit6');
d(ScanCode.Digit7, 'Digit7');
d(ScanCode.Digit8, 'Digit8');
d(ScanCode.Digit9, 'Digit9');
d(ScanCode.Digit0, 'Digit0');
d(ScanCode.Enter, 'Enter');
d(ScanCode.Escape, 'Escape');
d(ScanCode.Backspace, 'Backspace');
d(ScanCode.Tab, 'Tab');
d(ScanCode.Space, 'Space');
d(ScanCode.Minus, 'Minus');
d(ScanCode.Equal, 'Equal');
d(ScanCode.BracketLeft, 'BracketLeft');
d(ScanCode.BracketRight, 'BracketRight');
d(ScanCode.Backslash, 'Backslash');
d(ScanCode.IntlHash, 'IntlHash');
d(ScanCode.Semicolon, 'Semicolon');
d(ScanCode.Quote, 'Quote');
d(ScanCode.Backquote, 'Backquote');
d(ScanCode.Comma, 'Comma');
d(ScanCode.Period, 'Period');
d(ScanCode.Slash, 'Slash');
d(ScanCode.CapsLock, 'CapsLock');
d(ScanCode.F1, 'F1');
d(ScanCode.F2, 'F2');
d(ScanCode.F3, 'F3');
d(ScanCode.F4, 'F4');
d(ScanCode.F5, 'F5');
d(ScanCode.F6, 'F6');
d(ScanCode.F7, 'F7');
d(ScanCode.F8, 'F8');
d(ScanCode.F9, 'F9');
d(ScanCode.F10, 'F10');
d(ScanCode.F11, 'F11');
d(ScanCode.F12, 'F12');
d(ScanCode.PrintScreen, 'PrintScreen');
d(ScanCode.ScrollLock, 'ScrollLock');
d(ScanCode.Pause, 'Pause');
d(ScanCode.Insert, 'Insert');
d(ScanCode.Home, 'Home');
d(ScanCode.PageUp, 'PageUp');
d(ScanCode.Delete, 'Delete');
d(ScanCode.End, 'End');
d(ScanCode.PageDown, 'PageDown');
d(ScanCode.ArrowRight, 'ArrowRight');
d(ScanCode.ArrowLeft, 'ArrowLeft');
d(ScanCode.ArrowDown, 'ArrowDown');
d(ScanCode.ArrowUp, 'ArrowUp');
d(ScanCode.NumLock, 'NumLock');
d(ScanCode.NumpadDivide, 'NumpadDivide');
d(ScanCode.NumpadMultiply, 'NumpadMultiply');
d(ScanCode.NumpadSubtract, 'NumpadSubtract');
d(ScanCode.NumpadAdd, 'NumpadAdd');
d(ScanCode.NumpadEnter, 'NumpadEnter');
d(ScanCode.Numpad1, 'Numpad1');
d(ScanCode.Numpad2, 'Numpad2');
d(ScanCode.Numpad3, 'Numpad3');
d(ScanCode.Numpad4, 'Numpad4');
d(ScanCode.Numpad5, 'Numpad5');
d(ScanCode.Numpad6, 'Numpad6');
d(ScanCode.Numpad7, 'Numpad7');
d(ScanCode.Numpad8, 'Numpad8');
d(ScanCode.Numpad9, 'Numpad9');
d(ScanCode.Numpad0, 'Numpad0');
d(ScanCode.NumpadDecimal, 'NumpadDecimal');
d(ScanCode.IntlBackslash, 'IntlBackslash');
d(ScanCode.ContextMenu, 'ContextMenu');
d(ScanCode.Power, 'Power');
d(ScanCode.NumpadEqual, 'NumpadEqual');
d(ScanCode.F13, 'F13');
d(ScanCode.F14, 'F14');
d(ScanCode.F15, 'F15');
d(ScanCode.F16, 'F16');
d(ScanCode.F17, 'F17');
d(ScanCode.F18, 'F18');
d(ScanCode.F19, 'F19');
d(ScanCode.F20, 'F20');
d(ScanCode.F21, 'F21');
d(ScanCode.F22, 'F22');
d(ScanCode.F23, 'F23');
d(ScanCode.F24, 'F24');
d(ScanCode.Open, 'Open');
d(ScanCode.Help, 'Help');
d(ScanCode.Select, 'Select');
d(ScanCode.Again, 'Again');
d(ScanCode.Undo, 'Undo');
d(ScanCode.Cut, 'Cut');
d(ScanCode.Copy, 'Copy');
d(ScanCode.Paste, 'Paste');
d(ScanCode.Find, 'Find');
d(ScanCode.AudioVolumeMute, 'AudioVolumeMute');
d(ScanCode.AudioVolumeUp, 'AudioVolumeUp');
d(ScanCode.AudioVolumeDown, 'AudioVolumeDown');
d(ScanCode.NumpadComma, 'NumpadComma');
d(ScanCode.IntlRo, 'IntlRo');
d(ScanCode.KanaMode, 'KanaMode');
d(ScanCode.IntlYen, 'IntlYen');
d(ScanCode.Convert, 'Convert');
d(ScanCode.NonConvert, 'NonConvert');
d(ScanCode.Lang1, 'Lang1');
d(ScanCode.Lang2, 'Lang2');
d(ScanCode.Lang3, 'Lang3');
d(ScanCode.Lang4, 'Lang4');
d(ScanCode.Lang5, 'Lang5');
d(ScanCode.Abort, 'Abort');
d(ScanCode.Props, 'Props');
d(ScanCode.NumpadParenLeft, 'NumpadParenLeft');
d(ScanCode.NumpadParenRight, 'NumpadParenRight');
d(ScanCode.NumpadBackspace, 'NumpadBackspace');
d(ScanCode.NumpadMemoryStore, 'NumpadMemoryStore');
d(ScanCode.NumpadMemoryRecall, 'NumpadMemoryRecall');
d(ScanCode.NumpadMemoryClear, 'NumpadMemoryClear');
d(ScanCode.NumpadMemoryAdd, 'NumpadMemoryAdd');
d(ScanCode.NumpadMemorySubtract, 'NumpadMemorySubtract');
d(ScanCode.NumpadClear, 'NumpadClear');
d(ScanCode.NumpadClearEntry, 'NumpadClearEntry');
d(ScanCode.ControlLeft, 'ControlLeft');
d(ScanCode.ShiftLeft, 'ShiftLeft');
d(ScanCode.AltLeft, 'AltLeft');
d(ScanCode.MetaLeft, 'MetaLeft');
d(ScanCode.ControlRight, 'ControlRight');
d(ScanCode.ShiftRight, 'ShiftRight');
d(ScanCode.AltRight, 'AltRight');
d(ScanCode.MetaRight, 'MetaRight');
d(ScanCode.BrightnessUp, 'BrightnessUp');
d(ScanCode.BrightnessDown, 'BrightnessDown');
d(ScanCode.MediaPlay, 'MediaPlay');
d(ScanCode.MediaRecord, 'MediaRecord');
d(ScanCode.MediaFastForward, 'MediaFastForward');
d(ScanCode.MediaRewind, 'MediaRewind');
d(ScanCode.MediaTrackNext, 'MediaTrackNext');
d(ScanCode.MediaTrackPrevious, 'MediaTrackPrevious');
d(ScanCode.MediaStop, 'MediaStop');
d(ScanCode.Eject, 'Eject');
d(ScanCode.MediaPlayPause, 'MediaPlayPause');
d(ScanCode.MediaSelect, 'MediaSelect');
d(ScanCode.LaunchMail, 'LaunchMail');
d(ScanCode.LaunchApp2, 'LaunchApp2');
d(ScanCode.LaunchApp1, 'LaunchApp1');
d(ScanCode.SelectTask, 'SelectTask');
d(ScanCode.LaunchScreenSaver, 'LaunchScreenSaver');
d(ScanCode.BrowserSearch, 'BrowserSearch');
d(ScanCode.BrowserHome, 'BrowserHome');
d(ScanCode.BrowserBack, 'BrowserBack');
d(ScanCode.BrowserForward, 'BrowserForward');
d(ScanCode.BrowserStop, 'BrowserStop');
d(ScanCode.BrowserRefresh, 'BrowserRefresh');
d(ScanCode.BrowserFavorites, 'BrowserFavorites');
d(ScanCode.ZoomToggle, 'ZoomToggle');
d(ScanCode.MailReply, 'MailReply');
d(ScanCode.MailForward, 'MailForward');
d(ScanCode.MailSend, 'MailSend');
})();
(function () {
for (let i = 0; i <= ScanCode.MAX_VALUE; i++) {
IMMUTABLE_CODE_TO_KEY_CODE[i] = -1;
}
for (let i = 0; i <= KeyCode.MAX_VALUE; i++) {
IMMUTABLE_KEY_CODE_TO_CODE[i] = -1;
}
function define(code: ScanCode, keyCode: KeyCode): void {
IMMUTABLE_CODE_TO_KEY_CODE[code] = keyCode;
if (
(keyCode !== KeyCode.Unknown)
&& (keyCode !== KeyCode.Enter)
&& (keyCode !== KeyCode.Ctrl)
&& (keyCode !== KeyCode.Shift)
&& (keyCode !== KeyCode.Alt)
&& (keyCode !== KeyCode.Meta)
) {
IMMUTABLE_KEY_CODE_TO_CODE[keyCode] = code;
}
}
// Manually added due to the exclusion above (due to duplication with NumpadEnter)
IMMUTABLE_KEY_CODE_TO_CODE[KeyCode.Enter] = ScanCode.Enter;
define(ScanCode.None, KeyCode.Unknown);
define(ScanCode.Hyper, KeyCode.Unknown);
define(ScanCode.Super, KeyCode.Unknown);
define(ScanCode.Fn, KeyCode.Unknown);
define(ScanCode.FnLock, KeyCode.Unknown);
define(ScanCode.Suspend, KeyCode.Unknown);
define(ScanCode.Resume, KeyCode.Unknown);
define(ScanCode.Turbo, KeyCode.Unknown);
define(ScanCode.Sleep, KeyCode.Unknown);
define(ScanCode.WakeUp, KeyCode.Unknown);
// define(ScanCode.KeyA, KeyCode.Unknown);
// define(ScanCode.KeyB, KeyCode.Unknown);
// define(ScanCode.KeyC, KeyCode.Unknown);
// define(ScanCode.KeyD, KeyCode.Unknown);
// define(ScanCode.KeyE, KeyCode.Unknown);
// define(ScanCode.KeyF, KeyCode.Unknown);
// define(ScanCode.KeyG, KeyCode.Unknown);
// define(ScanCode.KeyH, KeyCode.Unknown);
// define(ScanCode.KeyI, KeyCode.Unknown);
// define(ScanCode.KeyJ, KeyCode.Unknown);
// define(ScanCode.KeyK, KeyCode.Unknown);
// define(ScanCode.KeyL, KeyCode.Unknown);
// define(ScanCode.KeyM, KeyCode.Unknown);
// define(ScanCode.KeyN, KeyCode.Unknown);
// define(ScanCode.KeyO, KeyCode.Unknown);
// define(ScanCode.KeyP, KeyCode.Unknown);
// define(ScanCode.KeyQ, KeyCode.Unknown);
// define(ScanCode.KeyR, KeyCode.Unknown);
// define(ScanCode.KeyS, KeyCode.Unknown);
// define(ScanCode.KeyT, KeyCode.Unknown);
// define(ScanCode.KeyU, KeyCode.Unknown);
// define(ScanCode.KeyV, KeyCode.Unknown);
// define(ScanCode.KeyW, KeyCode.Unknown);
// define(ScanCode.KeyX, KeyCode.Unknown);
// define(ScanCode.KeyY, KeyCode.Unknown);
// define(ScanCode.KeyZ, KeyCode.Unknown);
// define(ScanCode.Digit1, KeyCode.Unknown);
// define(ScanCode.Digit2, KeyCode.Unknown);
// define(ScanCode.Digit3, KeyCode.Unknown);
// define(ScanCode.Digit4, KeyCode.Unknown);
// define(ScanCode.Digit5, KeyCode.Unknown);
// define(ScanCode.Digit6, KeyCode.Unknown);
// define(ScanCode.Digit7, KeyCode.Unknown);
// define(ScanCode.Digit8, KeyCode.Unknown);
// define(ScanCode.Digit9, KeyCode.Unknown);
// define(ScanCode.Digit0, KeyCode.Unknown);
define(ScanCode.Enter, KeyCode.Enter);
define(ScanCode.Escape, KeyCode.Escape);
define(ScanCode.Backspace, KeyCode.Backspace);
define(ScanCode.Tab, KeyCode.Tab);
define(ScanCode.Space, KeyCode.Space);
// define(ScanCode.Minus, KeyCode.Unknown);
// define(ScanCode.Equal, KeyCode.Unknown);
// define(ScanCode.BracketLeft, KeyCode.Unknown);
// define(ScanCode.BracketRight, KeyCode.Unknown);
// define(ScanCode.Backslash, KeyCode.Unknown);
// define(ScanCode.IntlHash, KeyCode.Unknown);
// define(ScanCode.Semicolon, KeyCode.Unknown);
// define(ScanCode.Quote, KeyCode.Unknown);
// define(ScanCode.Backquote, KeyCode.Unknown);
// define(ScanCode.Comma, KeyCode.Unknown);
// define(ScanCode.Period, KeyCode.Unknown);
// define(ScanCode.Slash, KeyCode.Unknown);
define(ScanCode.CapsLock, KeyCode.CapsLock);
define(ScanCode.F1, KeyCode.F1);
define(ScanCode.F2, KeyCode.F2);
define(ScanCode.F3, KeyCode.F3);
define(ScanCode.F4, KeyCode.F4);
define(ScanCode.F5, KeyCode.F5);
define(ScanCode.F6, KeyCode.F6);
define(ScanCode.F7, KeyCode.F7);
define(ScanCode.F8, KeyCode.F8);
define(ScanCode.F9, KeyCode.F9);
define(ScanCode.F10, KeyCode.F10);
define(ScanCode.F11, KeyCode.F11);
define(ScanCode.F12, KeyCode.F12);
define(ScanCode.PrintScreen, KeyCode.Unknown);
define(ScanCode.ScrollLock, KeyCode.ScrollLock);
define(ScanCode.Pause, KeyCode.PauseBreak);
define(ScanCode.Insert, KeyCode.Insert);
define(ScanCode.Home, KeyCode.Home);
define(ScanCode.PageUp, KeyCode.PageUp);
define(ScanCode.Delete, KeyCode.Delete);
define(ScanCode.End, KeyCode.End);
define(ScanCode.PageDown, KeyCode.PageDown);
define(ScanCode.ArrowRight, KeyCode.RightArrow);
define(ScanCode.ArrowLeft, KeyCode.LeftArrow);
define(ScanCode.ArrowDown, KeyCode.DownArrow);
define(ScanCode.ArrowUp, KeyCode.UpArrow);
define(ScanCode.NumLock, KeyCode.NumLock);
define(ScanCode.NumpadDivide, KeyCode.NUMPAD_DIVIDE);
define(ScanCode.NumpadMultiply, KeyCode.NUMPAD_MULTIPLY);
define(ScanCode.NumpadSubtract, KeyCode.NUMPAD_SUBTRACT);
define(ScanCode.NumpadAdd, KeyCode.NUMPAD_ADD);
define(ScanCode.NumpadEnter, KeyCode.Enter); // Duplicate
define(ScanCode.Numpad1, KeyCode.NUMPAD_1);
define(ScanCode.Numpad2, KeyCode.NUMPAD_2);
define(ScanCode.Numpad3, KeyCode.NUMPAD_3);
define(ScanCode.Numpad4, KeyCode.NUMPAD_4);
define(ScanCode.Numpad5, KeyCode.NUMPAD_5);
define(ScanCode.Numpad6, KeyCode.NUMPAD_6);
define(ScanCode.Numpad7, KeyCode.NUMPAD_7);
define(ScanCode.Numpad8, KeyCode.NUMPAD_8);
define(ScanCode.Numpad9, KeyCode.NUMPAD_9);
define(ScanCode.Numpad0, KeyCode.NUMPAD_0);
define(ScanCode.NumpadDecimal, KeyCode.NUMPAD_DECIMAL);
// define(ScanCode.IntlBackslash, KeyCode.Unknown);
define(ScanCode.ContextMenu, KeyCode.ContextMenu);
define(ScanCode.Power, KeyCode.Unknown);
define(ScanCode.NumpadEqual, KeyCode.Unknown);
define(ScanCode.F13, KeyCode.F13);
define(ScanCode.F14, KeyCode.F14);
define(ScanCode.F15, KeyCode.F15);
define(ScanCode.F16, KeyCode.F16);
define(ScanCode.F17, KeyCode.F17);
define(ScanCode.F18, KeyCode.F18);
define(ScanCode.F19, KeyCode.F19);
define(ScanCode.F20, KeyCode.Unknown);
define(ScanCode.F21, KeyCode.Unknown);
define(ScanCode.F22, KeyCode.Unknown);
define(ScanCode.F23, KeyCode.Unknown);
define(ScanCode.F24, KeyCode.Unknown);
define(ScanCode.Open, KeyCode.Unknown);
define(ScanCode.Help, KeyCode.Unknown);
define(ScanCode.Select, KeyCode.Unknown);
define(ScanCode.Again, KeyCode.Unknown);
define(ScanCode.Undo, KeyCode.Unknown);
define(ScanCode.Cut, KeyCode.Unknown);
define(ScanCode.Copy, KeyCode.Unknown);
define(ScanCode.Paste, KeyCode.Unknown);
define(ScanCode.Find, KeyCode.Unknown);
define(ScanCode.AudioVolumeMute, KeyCode.Unknown);
define(ScanCode.AudioVolumeUp, KeyCode.Unknown);
define(ScanCode.AudioVolumeDown, KeyCode.Unknown);
define(ScanCode.NumpadComma, KeyCode.NUMPAD_SEPARATOR);
// define(ScanCode.IntlRo, KeyCode.Unknown);
define(ScanCode.KanaMode, KeyCode.Unknown);
// define(ScanCode.IntlYen, KeyCode.Unknown);
define(ScanCode.Convert, KeyCode.Unknown);
define(ScanCode.NonConvert, KeyCode.Unknown);
define(ScanCode.Lang1, KeyCode.Unknown);
define(ScanCode.Lang2, KeyCode.Unknown);
define(ScanCode.Lang3, KeyCode.Unknown);
define(ScanCode.Lang4, KeyCode.Unknown);
define(ScanCode.Lang5, KeyCode.Unknown);
define(ScanCode.Abort, KeyCode.Unknown);
define(ScanCode.Props, KeyCode.Unknown);
define(ScanCode.NumpadParenLeft, KeyCode.Unknown);
define(ScanCode.NumpadParenRight, KeyCode.Unknown);
define(ScanCode.NumpadBackspace, KeyCode.Unknown);
define(ScanCode.NumpadMemoryStore, KeyCode.Unknown);
define(ScanCode.NumpadMemoryRecall, KeyCode.Unknown);
define(ScanCode.NumpadMemoryClear, KeyCode.Unknown);
define(ScanCode.NumpadMemoryAdd, KeyCode.Unknown);
define(ScanCode.NumpadMemorySubtract, KeyCode.Unknown);
define(ScanCode.NumpadClear, KeyCode.Unknown);
define(ScanCode.NumpadClearEntry, KeyCode.Unknown);
define(ScanCode.ControlLeft, KeyCode.Ctrl); // Duplicate
define(ScanCode.ShiftLeft, KeyCode.Shift); // Duplicate
define(ScanCode.AltLeft, KeyCode.Alt); // Duplicate
define(ScanCode.MetaLeft, KeyCode.Meta); // Duplicate
define(ScanCode.ControlRight, KeyCode.Ctrl); // Duplicate
define(ScanCode.ShiftRight, KeyCode.Shift); // Duplicate
define(ScanCode.AltRight, KeyCode.Alt); // Duplicate
define(ScanCode.MetaRight, KeyCode.Meta); // Duplicate
define(ScanCode.BrightnessUp, KeyCode.Unknown);
define(ScanCode.BrightnessDown, KeyCode.Unknown);
define(ScanCode.MediaPlay, KeyCode.Unknown);
define(ScanCode.MediaRecord, KeyCode.Unknown);
define(ScanCode.MediaFastForward, KeyCode.Unknown);
define(ScanCode.MediaRewind, KeyCode.Unknown);
define(ScanCode.MediaTrackNext, KeyCode.Unknown);
define(ScanCode.MediaTrackPrevious, KeyCode.Unknown);
define(ScanCode.MediaStop, KeyCode.Unknown);
define(ScanCode.Eject, KeyCode.Unknown);
define(ScanCode.MediaPlayPause, KeyCode.Unknown);
define(ScanCode.MediaSelect, KeyCode.Unknown);
define(ScanCode.LaunchMail, KeyCode.Unknown);
define(ScanCode.LaunchApp2, KeyCode.Unknown);
define(ScanCode.LaunchApp1, KeyCode.Unknown);
define(ScanCode.SelectTask, KeyCode.Unknown);
define(ScanCode.LaunchScreenSaver, KeyCode.Unknown);
define(ScanCode.BrowserSearch, KeyCode.Unknown);
define(ScanCode.BrowserHome, KeyCode.Unknown);
define(ScanCode.BrowserBack, KeyCode.Unknown);
define(ScanCode.BrowserForward, KeyCode.Unknown);
define(ScanCode.BrowserStop, KeyCode.Unknown);
define(ScanCode.BrowserRefresh, KeyCode.Unknown);
define(ScanCode.BrowserFavorites, KeyCode.Unknown);
define(ScanCode.ZoomToggle, KeyCode.Unknown);
define(ScanCode.MailReply, KeyCode.Unknown);
define(ScanCode.MailForward, KeyCode.Unknown);
define(ScanCode.MailSend, KeyCode.Unknown);
})();

View File

@@ -0,0 +1,754 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { KeyCode, KeyCodeUtils, ResolvedKeybinding, Keybinding, SimpleKeybinding, KeybindingType, ResolvedKeybindingPart } from 'vs/base/common/keyCodes';
import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE, ScanCodeBinding } from 'vs/workbench/services/keybinding/common/scanCode';
import { CharCode } from 'vs/base/common/charCode';
import { UILabelProvider, AriaLabelProvider, ElectronAcceleratorLabelProvider, UserSettingsLabelProvider } from 'vs/base/common/keybindingLabels';
import { OperatingSystem } from 'vs/base/common/platform';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
export interface IWindowsKeyMapping {
vkey: string;
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
}
function windowsKeyMappingEquals(a: IWindowsKeyMapping, b: IWindowsKeyMapping): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
return (
a.vkey === b.vkey
&& a.value === b.value
&& a.withShift === b.withShift
&& a.withAltGr === b.withAltGr
&& a.withShiftAltGr === b.withShiftAltGr
);
}
export interface IWindowsKeyboardMapping {
[scanCode: string]: IWindowsKeyMapping;
}
export function windowsKeyboardMappingEquals(a: IWindowsKeyboardMapping, b: IWindowsKeyboardMapping): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
for (let scanCode = 0; scanCode < ScanCode.MAX_VALUE; scanCode++) {
const strScanCode = ScanCodeUtils.toString(scanCode);
const aEntry = a[strScanCode];
const bEntry = b[strScanCode];
if (!windowsKeyMappingEquals(aEntry, bEntry)) {
return false;
}
}
return true;
}
const LOG = false;
function log(str: string): void {
if (LOG) {
console.info(str);
}
}
const NATIVE_KEY_CODE_TO_KEY_CODE: { [nativeKeyCode: string]: KeyCode; } = _getNativeMap();
export interface IScanCodeMapping {
scanCode: ScanCode;
keyCode: KeyCode;
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
}
export class WindowsNativeResolvedKeybinding extends ResolvedKeybinding {
private readonly _mapper: WindowsKeyboardMapper;
private readonly _firstPart: SimpleKeybinding;
private readonly _chordPart: SimpleKeybinding;
constructor(mapper: WindowsKeyboardMapper, firstPart: SimpleKeybinding, chordPart: SimpleKeybinding) {
super();
this._mapper = mapper;
this._firstPart = firstPart;
this._chordPart = chordPart;
}
private _getUILabelForKeybinding(keybinding: SimpleKeybinding): string {
if (!keybinding) {
return null;
}
if (keybinding.isDuplicateModifierCase()) {
return '';
}
return this._mapper.getUILabelForKeyCode(keybinding.keyCode);
}
public getLabel(): string {
let firstPart = this._getUILabelForKeybinding(this._firstPart);
let chordPart = this._getUILabelForKeybinding(this._chordPart);
return UILabelProvider.toLabel(this._firstPart, firstPart, this._chordPart, chordPart, OperatingSystem.Windows);
}
private _getUSLabelForKeybinding(keybinding: SimpleKeybinding): string {
if (!keybinding) {
return null;
}
if (keybinding.isDuplicateModifierCase()) {
return '';
}
return KeyCodeUtils.toString(keybinding.keyCode);
}
public getUSLabel(): string {
let firstPart = this._getUSLabelForKeybinding(this._firstPart);
let chordPart = this._getUSLabelForKeybinding(this._chordPart);
return UILabelProvider.toLabel(this._firstPart, firstPart, this._chordPart, chordPart, OperatingSystem.Windows);
}
private _getAriaLabelForKeybinding(keybinding: SimpleKeybinding): string {
if (!keybinding) {
return null;
}
if (keybinding.isDuplicateModifierCase()) {
return '';
}
return this._mapper.getAriaLabelForKeyCode(keybinding.keyCode);
}
public getAriaLabel(): string {
let firstPart = this._getAriaLabelForKeybinding(this._firstPart);
let chordPart = this._getAriaLabelForKeybinding(this._chordPart);
return AriaLabelProvider.toLabel(this._firstPart, firstPart, this._chordPart, chordPart, OperatingSystem.Windows);
}
private _keyCodeToElectronAccelerator(keyCode: KeyCode): string {
if (keyCode >= KeyCode.NUMPAD_0 && keyCode <= KeyCode.NUMPAD_DIVIDE) {
// Electron cannot handle numpad keys
return null;
}
switch (keyCode) {
case KeyCode.UpArrow:
return 'Up';
case KeyCode.DownArrow:
return 'Down';
case KeyCode.LeftArrow:
return 'Left';
case KeyCode.RightArrow:
return 'Right';
}
// electron menus always do the correct rendering on Windows
return KeyCodeUtils.toString(keyCode);
}
private _getElectronAcceleratorLabelForKeybinding(keybinding: SimpleKeybinding): string {
if (!keybinding) {
return null;
}
if (keybinding.isDuplicateModifierCase()) {
return null;
}
return this._keyCodeToElectronAccelerator(keybinding.keyCode);
}
public getElectronAccelerator(): string {
if (this._chordPart !== null) {
// Electron cannot handle chords
return null;
}
let firstPart = this._getElectronAcceleratorLabelForKeybinding(this._firstPart);
return ElectronAcceleratorLabelProvider.toLabel(this._firstPart, firstPart, null, null, OperatingSystem.Windows);
}
private _getUserSettingsLabelForKeybinding(keybinding: SimpleKeybinding): string {
if (!keybinding) {
return null;
}
if (keybinding.isDuplicateModifierCase()) {
return '';
}
return this._mapper.getUserSettingsLabelForKeyCode(keybinding.keyCode);
}
public getUserSettingsLabel(): string {
let firstPart = this._getUserSettingsLabelForKeybinding(this._firstPart);
let chordPart = this._getUserSettingsLabelForKeybinding(this._chordPart);
let result = UserSettingsLabelProvider.toLabel(this._firstPart, firstPart, this._chordPart, chordPart, OperatingSystem.Windows);
return (result ? result.toLowerCase() : result);
}
public isWYSIWYG(): boolean {
if (this._firstPart && !this._isWYSIWYG(this._firstPart.keyCode)) {
return false;
}
if (this._chordPart && !this._isWYSIWYG(this._chordPart.keyCode)) {
return false;
}
return true;
}
private _isWYSIWYG(keyCode: KeyCode): boolean {
if (
keyCode === KeyCode.LeftArrow
|| keyCode === KeyCode.UpArrow
|| keyCode === KeyCode.RightArrow
|| keyCode === KeyCode.DownArrow
) {
return true;
}
const ariaLabel = this._mapper.getAriaLabelForKeyCode(keyCode);
const userSettingsLabel = this._mapper.getUserSettingsLabelForKeyCode(keyCode);
return (ariaLabel === userSettingsLabel);
}
public isChord(): boolean {
return (this._chordPart ? true : false);
}
public getParts(): [ResolvedKeybindingPart, ResolvedKeybindingPart] {
return [
this._toResolvedKeybindingPart(this._firstPart),
this._toResolvedKeybindingPart(this._chordPart)
];
}
private _toResolvedKeybindingPart(keybinding: SimpleKeybinding): ResolvedKeybindingPart {
if (!keybinding) {
return null;
}
return new ResolvedKeybindingPart(
keybinding.ctrlKey,
keybinding.shiftKey,
keybinding.altKey,
keybinding.metaKey,
this._getUILabelForKeybinding(keybinding),
this._getAriaLabelForKeybinding(keybinding)
);
}
public getDispatchParts(): [string, string] {
let firstPart = this._firstPart ? this._getDispatchStr(this._firstPart) : null;
let chordPart = this._chordPart ? this._getDispatchStr(this._chordPart) : null;
return [firstPart, chordPart];
}
private _getDispatchStr(keybinding: SimpleKeybinding): string {
if (keybinding.isModifierKey()) {
return null;
}
let result = '';
if (keybinding.ctrlKey) {
result += 'ctrl+';
}
if (keybinding.shiftKey) {
result += 'shift+';
}
if (keybinding.altKey) {
result += 'alt+';
}
if (keybinding.metaKey) {
result += 'meta+';
}
result += KeyCodeUtils.toString(keybinding.keyCode);
return result;
}
private static getProducedCharCode(kb: ScanCodeBinding, mapping: IScanCodeMapping): string {
if (!mapping) {
return null;
}
if (kb.ctrlKey && kb.shiftKey && kb.altKey) {
return mapping.withShiftAltGr;
}
if (kb.ctrlKey && kb.altKey) {
return mapping.withAltGr;
}
if (kb.shiftKey) {
return mapping.withShift;
}
return mapping.value;
}
public static getProducedChar(kb: ScanCodeBinding, mapping: IScanCodeMapping): string {
const char = this.getProducedCharCode(kb, mapping);
if (char === null || char.length === 0) {
return ' --- ';
}
return ' ' + char + ' ';
}
}
export class WindowsKeyboardMapper implements IKeyboardMapper {
public readonly isUSStandard: boolean;
private readonly _codeInfo: IScanCodeMapping[];
private readonly _scanCodeToKeyCode: KeyCode[];
private readonly _keyCodeToLabel: string[] = [];
private readonly _keyCodeExists: boolean[];
constructor(isUSStandard: boolean, rawMappings: IWindowsKeyboardMapping) {
this.isUSStandard = isUSStandard;
this._scanCodeToKeyCode = [];
this._keyCodeToLabel = [];
this._keyCodeExists = [];
this._keyCodeToLabel[KeyCode.Unknown] = KeyCodeUtils.toString(KeyCode.Unknown);
for (let scanCode = ScanCode.None; scanCode < ScanCode.MAX_VALUE; scanCode++) {
const immutableKeyCode = IMMUTABLE_CODE_TO_KEY_CODE[scanCode];
if (immutableKeyCode !== -1) {
this._scanCodeToKeyCode[scanCode] = immutableKeyCode;
this._keyCodeToLabel[immutableKeyCode] = KeyCodeUtils.toString(immutableKeyCode);
this._keyCodeExists[immutableKeyCode] = true;
}
}
let producesLetter: boolean[] = [];
this._codeInfo = [];
for (let strCode in rawMappings) {
if (rawMappings.hasOwnProperty(strCode)) {
const scanCode = ScanCodeUtils.toEnum(strCode);
if (scanCode === ScanCode.None) {
log(`Unknown scanCode ${strCode} in mapping.`);
continue;
}
const rawMapping = rawMappings[strCode];
const immutableKeyCode = IMMUTABLE_CODE_TO_KEY_CODE[scanCode];
if (immutableKeyCode !== -1) {
const keyCode = NATIVE_KEY_CODE_TO_KEY_CODE[rawMapping.vkey] || KeyCode.Unknown;
if (keyCode === KeyCode.Unknown || immutableKeyCode === keyCode) {
continue;
}
if (scanCode !== ScanCode.NumpadComma) {
// Looks like ScanCode.NumpadComma doesn't always map to KeyCode.NUMPAD_SEPARATOR
// e.g. on POR - PTB
continue;
}
}
const value = rawMapping.value;
const withShift = rawMapping.withShift;
const withAltGr = rawMapping.withAltGr;
const withShiftAltGr = rawMapping.withShiftAltGr;
const keyCode = NATIVE_KEY_CODE_TO_KEY_CODE[rawMapping.vkey] || KeyCode.Unknown;
const mapping: IScanCodeMapping = {
scanCode: scanCode,
keyCode: keyCode,
value: value,
withShift: withShift,
withAltGr: withAltGr,
withShiftAltGr: withShiftAltGr,
};
this._codeInfo[scanCode] = mapping;
this._scanCodeToKeyCode[scanCode] = keyCode;
if (keyCode === KeyCode.Unknown) {
continue;
}
this._keyCodeExists[keyCode] = true;
if (value.length === 0) {
// This key does not produce strings
this._keyCodeToLabel[keyCode] = null;
}
else if (value.length > 1) {
// This key produces a letter representable with multiple UTF-16 code units.
this._keyCodeToLabel[keyCode] = value;
}
else {
const charCode = value.charCodeAt(0);
if (charCode >= CharCode.a && charCode <= CharCode.z) {
const upperCaseValue = CharCode.A + (charCode - CharCode.a);
producesLetter[upperCaseValue] = true;
this._keyCodeToLabel[keyCode] = String.fromCharCode(CharCode.A + (charCode - CharCode.a));
}
else if (charCode >= CharCode.A && charCode <= CharCode.Z) {
producesLetter[charCode] = true;
this._keyCodeToLabel[keyCode] = value;
}
else {
this._keyCodeToLabel[keyCode] = value;
}
}
}
}
// Handle keyboard layouts where latin characters are not produced e.g. Cyrillic
const _registerLetterIfMissing = (charCode: CharCode, keyCode: KeyCode): void => {
if (!producesLetter[charCode]) {
this._keyCodeToLabel[keyCode] = String.fromCharCode(charCode);
}
};
_registerLetterIfMissing(CharCode.A, KeyCode.KEY_A);
_registerLetterIfMissing(CharCode.B, KeyCode.KEY_B);
_registerLetterIfMissing(CharCode.C, KeyCode.KEY_C);
_registerLetterIfMissing(CharCode.D, KeyCode.KEY_D);
_registerLetterIfMissing(CharCode.E, KeyCode.KEY_E);
_registerLetterIfMissing(CharCode.F, KeyCode.KEY_F);
_registerLetterIfMissing(CharCode.G, KeyCode.KEY_G);
_registerLetterIfMissing(CharCode.H, KeyCode.KEY_H);
_registerLetterIfMissing(CharCode.I, KeyCode.KEY_I);
_registerLetterIfMissing(CharCode.J, KeyCode.KEY_J);
_registerLetterIfMissing(CharCode.K, KeyCode.KEY_K);
_registerLetterIfMissing(CharCode.L, KeyCode.KEY_L);
_registerLetterIfMissing(CharCode.M, KeyCode.KEY_M);
_registerLetterIfMissing(CharCode.N, KeyCode.KEY_N);
_registerLetterIfMissing(CharCode.O, KeyCode.KEY_O);
_registerLetterIfMissing(CharCode.P, KeyCode.KEY_P);
_registerLetterIfMissing(CharCode.Q, KeyCode.KEY_Q);
_registerLetterIfMissing(CharCode.R, KeyCode.KEY_R);
_registerLetterIfMissing(CharCode.S, KeyCode.KEY_S);
_registerLetterIfMissing(CharCode.T, KeyCode.KEY_T);
_registerLetterIfMissing(CharCode.U, KeyCode.KEY_U);
_registerLetterIfMissing(CharCode.V, KeyCode.KEY_V);
_registerLetterIfMissing(CharCode.W, KeyCode.KEY_W);
_registerLetterIfMissing(CharCode.X, KeyCode.KEY_X);
_registerLetterIfMissing(CharCode.Y, KeyCode.KEY_Y);
_registerLetterIfMissing(CharCode.Z, KeyCode.KEY_Z);
}
public dumpDebugInfo(): string {
let result: string[] = [];
let immutableSamples = [
ScanCode.ArrowUp,
ScanCode.Numpad0
];
let cnt = 0;
result.push(`-----------------------------------------------------------------------------------------------------------------------------------------`);
for (let scanCode = ScanCode.None; scanCode < ScanCode.MAX_VALUE; scanCode++) {
if (IMMUTABLE_CODE_TO_KEY_CODE[scanCode] !== -1) {
if (immutableSamples.indexOf(scanCode) === -1) {
continue;
}
}
if (cnt % 6 === 0) {
result.push(`| HW Code combination | Key | KeyCode combination | UI label | User settings | WYSIWYG |`);
result.push(`-----------------------------------------------------------------------------------------------------------------------------------------`);
}
cnt++;
const mapping = this._codeInfo[scanCode];
const strCode = ScanCodeUtils.toString(scanCode);
let mods = [0b000, 0b010, 0b101, 0b111];
for (let modIndex = 0; modIndex < mods.length; modIndex++) {
const mod = mods[modIndex];
const ctrlKey = (mod & 0b001) ? true : false;
const shiftKey = (mod & 0b010) ? true : false;
const altKey = (mod & 0b100) ? true : false;
const scanCodeBinding = new ScanCodeBinding(ctrlKey, shiftKey, altKey, false, scanCode);
const kb = this._resolveSimpleUserBinding(scanCodeBinding);
const strKeyCode = (kb ? KeyCodeUtils.toString(kb.keyCode) : null);
const resolvedKb = (kb ? new WindowsNativeResolvedKeybinding(this, kb, null) : null);
const outScanCode = `${ctrlKey ? 'Ctrl+' : ''}${shiftKey ? 'Shift+' : ''}${altKey ? 'Alt+' : ''}${strCode}`;
const ariaLabel = (resolvedKb ? resolvedKb.getAriaLabel() : null);
const outUILabel = (ariaLabel ? ariaLabel.replace(/Control\+/, 'Ctrl+') : null);
const outUserSettings = (resolvedKb ? resolvedKb.getUserSettingsLabel() : null);
const outKey = WindowsNativeResolvedKeybinding.getProducedChar(scanCodeBinding, mapping);
const outKb = (strKeyCode ? `${ctrlKey ? 'Ctrl+' : ''}${shiftKey ? 'Shift+' : ''}${altKey ? 'Alt+' : ''}${strKeyCode}` : null);
const isWYSIWYG = (resolvedKb ? resolvedKb.isWYSIWYG() : false);
const outWYSIWYG = (isWYSIWYG ? ' ' : ' NO ');
result.push(`| ${this._leftPad(outScanCode, 30)} | ${outKey} | ${this._leftPad(outKb, 25)} | ${this._leftPad(outUILabel, 25)} | ${this._leftPad(outUserSettings, 25)} | ${outWYSIWYG} |`);
}
result.push(`-----------------------------------------------------------------------------------------------------------------------------------------`);
}
return result.join('\n');
}
private _leftPad(str: string, cnt: number): string {
if (str === null) {
str = 'null';
}
while (str.length < cnt) {
str = ' ' + str;
}
return str;
}
public getUILabelForKeyCode(keyCode: KeyCode): string {
return this._getLabelForKeyCode(keyCode);
}
public getAriaLabelForKeyCode(keyCode: KeyCode): string {
return this._getLabelForKeyCode(keyCode);
}
public getUserSettingsLabelForKeyCode(keyCode: KeyCode): string {
if (this.isUSStandard) {
return KeyCodeUtils.toUserSettingsUS(keyCode);
}
return KeyCodeUtils.toUserSettingsGeneral(keyCode);
}
private _getLabelForKeyCode(keyCode: KeyCode): string {
return this._keyCodeToLabel[keyCode] || KeyCodeUtils.toString(KeyCode.Unknown);
}
public resolveKeybinding(keybinding: Keybinding): WindowsNativeResolvedKeybinding[] {
if (keybinding.type === KeybindingType.Chord) {
const firstPartKeyCode = keybinding.firstPart.keyCode;
const chordPartKeyCode = keybinding.chordPart.keyCode;
if (!this._keyCodeExists[firstPartKeyCode] || !this._keyCodeExists[chordPartKeyCode]) {
return [];
}
return [new WindowsNativeResolvedKeybinding(this, keybinding.firstPart, keybinding.chordPart)];
} else {
if (!this._keyCodeExists[keybinding.keyCode]) {
return [];
}
return [new WindowsNativeResolvedKeybinding(this, keybinding, null)];
}
}
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): WindowsNativeResolvedKeybinding {
const keybinding = new SimpleKeybinding(keyboardEvent.ctrlKey, keyboardEvent.shiftKey, keyboardEvent.altKey, keyboardEvent.metaKey, keyboardEvent.keyCode);
return new WindowsNativeResolvedKeybinding(this, keybinding, null);
}
private _resolveSimpleUserBinding(binding: SimpleKeybinding | ScanCodeBinding): SimpleKeybinding {
if (!binding) {
return null;
}
if (binding instanceof SimpleKeybinding) {
if (!this._keyCodeExists[binding.keyCode]) {
return null;
}
return binding;
}
const keyCode = this._scanCodeToKeyCode[binding.scanCode] || KeyCode.Unknown;
if (keyCode === KeyCode.Unknown || !this._keyCodeExists[keyCode]) {
return null;
}
return new SimpleKeybinding(binding.ctrlKey, binding.shiftKey, binding.altKey, binding.metaKey, keyCode);
}
public resolveUserBinding(firstPart: SimpleKeybinding | ScanCodeBinding, chordPart: SimpleKeybinding | ScanCodeBinding): ResolvedKeybinding[] {
const _firstPart = this._resolveSimpleUserBinding(firstPart);
const _chordPart = this._resolveSimpleUserBinding(chordPart);
if (_firstPart && _chordPart) {
return [new WindowsNativeResolvedKeybinding(this, _firstPart, _chordPart)];
}
if (_firstPart) {
return [new WindowsNativeResolvedKeybinding(this, _firstPart, null)];
}
return [];
}
}
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
// See https://github.com/Microsoft/node-native-keymap/blob/master/deps/chromium/keyboard_codes_win.h
function _getNativeMap() {
return {
VK_BACK: KeyCode.Backspace,
VK_TAB: KeyCode.Tab,
VK_CLEAR: KeyCode.Unknown, // MISSING
VK_RETURN: KeyCode.Enter,
VK_SHIFT: KeyCode.Shift,
VK_CONTROL: KeyCode.Ctrl,
VK_MENU: KeyCode.Alt,
VK_PAUSE: KeyCode.PauseBreak,
VK_CAPITAL: KeyCode.CapsLock,
VK_KANA: KeyCode.Unknown, // MISSING
VK_HANGUL: KeyCode.Unknown, // MISSING
VK_JUNJA: KeyCode.Unknown, // MISSING
VK_FINAL: KeyCode.Unknown, // MISSING
VK_HANJA: KeyCode.Unknown, // MISSING
VK_KANJI: KeyCode.Unknown, // MISSING
VK_ESCAPE: KeyCode.Escape,
VK_CONVERT: KeyCode.Unknown, // MISSING
VK_NONCONVERT: KeyCode.Unknown, // MISSING
VK_ACCEPT: KeyCode.Unknown, // MISSING
VK_MODECHANGE: KeyCode.Unknown, // MISSING
VK_SPACE: KeyCode.Space,
VK_PRIOR: KeyCode.PageUp,
VK_NEXT: KeyCode.PageDown,
VK_END: KeyCode.End,
VK_HOME: KeyCode.Home,
VK_LEFT: KeyCode.LeftArrow,
VK_UP: KeyCode.UpArrow,
VK_RIGHT: KeyCode.RightArrow,
VK_DOWN: KeyCode.DownArrow,
VK_SELECT: KeyCode.Unknown, // MISSING
VK_PRINT: KeyCode.Unknown, // MISSING
VK_EXECUTE: KeyCode.Unknown, // MISSING
VK_SNAPSHOT: KeyCode.Unknown, // MISSING
VK_INSERT: KeyCode.Insert,
VK_DELETE: KeyCode.Delete,
VK_HELP: KeyCode.Unknown, // MISSING
VK_0: KeyCode.KEY_0,
VK_1: KeyCode.KEY_1,
VK_2: KeyCode.KEY_2,
VK_3: KeyCode.KEY_3,
VK_4: KeyCode.KEY_4,
VK_5: KeyCode.KEY_5,
VK_6: KeyCode.KEY_6,
VK_7: KeyCode.KEY_7,
VK_8: KeyCode.KEY_8,
VK_9: KeyCode.KEY_9,
VK_A: KeyCode.KEY_A,
VK_B: KeyCode.KEY_B,
VK_C: KeyCode.KEY_C,
VK_D: KeyCode.KEY_D,
VK_E: KeyCode.KEY_E,
VK_F: KeyCode.KEY_F,
VK_G: KeyCode.KEY_G,
VK_H: KeyCode.KEY_H,
VK_I: KeyCode.KEY_I,
VK_J: KeyCode.KEY_J,
VK_K: KeyCode.KEY_K,
VK_L: KeyCode.KEY_L,
VK_M: KeyCode.KEY_M,
VK_N: KeyCode.KEY_N,
VK_O: KeyCode.KEY_O,
VK_P: KeyCode.KEY_P,
VK_Q: KeyCode.KEY_Q,
VK_R: KeyCode.KEY_R,
VK_S: KeyCode.KEY_S,
VK_T: KeyCode.KEY_T,
VK_U: KeyCode.KEY_U,
VK_V: KeyCode.KEY_V,
VK_W: KeyCode.KEY_W,
VK_X: KeyCode.KEY_X,
VK_Y: KeyCode.KEY_Y,
VK_Z: KeyCode.KEY_Z,
VK_LWIN: KeyCode.Meta,
VK_COMMAND: KeyCode.Meta,
VK_RWIN: KeyCode.Meta,
VK_APPS: KeyCode.Unknown, // MISSING
VK_SLEEP: KeyCode.Unknown, // MISSING
VK_NUMPAD0: KeyCode.NUMPAD_0,
VK_NUMPAD1: KeyCode.NUMPAD_1,
VK_NUMPAD2: KeyCode.NUMPAD_2,
VK_NUMPAD3: KeyCode.NUMPAD_3,
VK_NUMPAD4: KeyCode.NUMPAD_4,
VK_NUMPAD5: KeyCode.NUMPAD_5,
VK_NUMPAD6: KeyCode.NUMPAD_6,
VK_NUMPAD7: KeyCode.NUMPAD_7,
VK_NUMPAD8: KeyCode.NUMPAD_8,
VK_NUMPAD9: KeyCode.NUMPAD_9,
VK_MULTIPLY: KeyCode.NUMPAD_MULTIPLY,
VK_ADD: KeyCode.NUMPAD_ADD,
VK_SEPARATOR: KeyCode.NUMPAD_SEPARATOR,
VK_SUBTRACT: KeyCode.NUMPAD_SUBTRACT,
VK_DECIMAL: KeyCode.NUMPAD_DECIMAL,
VK_DIVIDE: KeyCode.NUMPAD_DIVIDE,
VK_F1: KeyCode.F1,
VK_F2: KeyCode.F2,
VK_F3: KeyCode.F3,
VK_F4: KeyCode.F4,
VK_F5: KeyCode.F5,
VK_F6: KeyCode.F6,
VK_F7: KeyCode.F7,
VK_F8: KeyCode.F8,
VK_F9: KeyCode.F9,
VK_F10: KeyCode.F10,
VK_F11: KeyCode.F11,
VK_F12: KeyCode.F12,
VK_F13: KeyCode.F13,
VK_F14: KeyCode.F14,
VK_F15: KeyCode.F15,
VK_F16: KeyCode.F16,
VK_F17: KeyCode.F17,
VK_F18: KeyCode.F18,
VK_F19: KeyCode.F19,
VK_F20: KeyCode.Unknown, // MISSING
VK_F21: KeyCode.Unknown, // MISSING
VK_F22: KeyCode.Unknown, // MISSING
VK_F23: KeyCode.Unknown, // MISSING
VK_F24: KeyCode.Unknown, // MISSING
VK_NUMLOCK: KeyCode.NumLock,
VK_SCROLL: KeyCode.ScrollLock,
VK_LSHIFT: KeyCode.Shift,
VK_RSHIFT: KeyCode.Shift,
VK_LCONTROL: KeyCode.Ctrl,
VK_RCONTROL: KeyCode.Ctrl,
VK_LMENU: KeyCode.Unknown, // MISSING
VK_RMENU: KeyCode.Unknown, // MISSING
VK_BROWSER_BACK: KeyCode.Unknown, // MISSING
VK_BROWSER_FORWARD: KeyCode.Unknown, // MISSING
VK_BROWSER_REFRESH: KeyCode.Unknown, // MISSING
VK_BROWSER_STOP: KeyCode.Unknown, // MISSING
VK_BROWSER_SEARCH: KeyCode.Unknown, // MISSING
VK_BROWSER_FAVORITES: KeyCode.Unknown, // MISSING
VK_BROWSER_HOME: KeyCode.Unknown, // MISSING
VK_VOLUME_MUTE: KeyCode.Unknown, // MISSING
VK_VOLUME_DOWN: KeyCode.Unknown, // MISSING
VK_VOLUME_UP: KeyCode.Unknown, // MISSING
VK_MEDIA_NEXT_TRACK: KeyCode.Unknown, // MISSING
VK_MEDIA_PREV_TRACK: KeyCode.Unknown, // MISSING
VK_MEDIA_STOP: KeyCode.Unknown, // MISSING
VK_MEDIA_PLAY_PAUSE: KeyCode.Unknown, // MISSING
VK_MEDIA_LAUNCH_MAIL: KeyCode.Unknown, // MISSING
VK_MEDIA_LAUNCH_MEDIA_SELECT: KeyCode.Unknown, // MISSING
VK_MEDIA_LAUNCH_APP1: KeyCode.Unknown, // MISSING
VK_MEDIA_LAUNCH_APP2: KeyCode.Unknown, // MISSING
VK_OEM_1: KeyCode.US_SEMICOLON,
VK_OEM_PLUS: KeyCode.US_EQUAL,
VK_OEM_COMMA: KeyCode.US_COMMA,
VK_OEM_MINUS: KeyCode.US_MINUS,
VK_OEM_PERIOD: KeyCode.US_DOT,
VK_OEM_2: KeyCode.US_SLASH,
VK_OEM_3: KeyCode.US_BACKTICK,
VK_ABNT_C1: KeyCode.ABNT_C1,
VK_ABNT_C2: KeyCode.ABNT_C2,
VK_OEM_4: KeyCode.US_OPEN_SQUARE_BRACKET,
VK_OEM_5: KeyCode.US_BACKSLASH,
VK_OEM_6: KeyCode.US_CLOSE_SQUARE_BRACKET,
VK_OEM_7: KeyCode.US_QUOTE,
VK_OEM_8: KeyCode.OEM_8,
VK_OEM_102: KeyCode.OEM_102,
VK_PROCESSKEY: KeyCode.Unknown, // MISSING
VK_PACKET: KeyCode.Unknown, // MISSING
VK_DBE_SBCSCHAR: KeyCode.Unknown, // MISSING
VK_DBE_DBCSCHAR: KeyCode.Unknown, // MISSING
VK_ATTN: KeyCode.Unknown, // MISSING
VK_CRSEL: KeyCode.Unknown, // MISSING
VK_EXSEL: KeyCode.Unknown, // MISSING
VK_EREOF: KeyCode.Unknown, // MISSING
VK_PLAY: KeyCode.Unknown, // MISSING
VK_ZOOM: KeyCode.Unknown, // MISSING
VK_NONAME: KeyCode.Unknown, // MISSING
VK_PA1: KeyCode.Unknown, // MISSING
VK_OEM_CLEAR: KeyCode.Unknown, // MISSING
VK_UNKNOWN: KeyCode.Unknown,
};
}

View File

@@ -0,0 +1,588 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { ResolvedKeybinding, Keybinding } from 'vs/base/common/keyCodes';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { toDisposable } from 'vs/base/common/lifecycle';
import { ExtensionMessageCollector, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService';
import { IStatusbarService } from 'vs/platform/statusbar/common/statusbar';
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IKeybindingEvent, IUserFriendlyKeybinding, KeybindingSource, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IKeybindingItem, KeybindingsRegistry, IKeybindingRule2 } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { keybindingsTelemetry } from 'vs/platform/telemetry/common/telemetryUtils';
import { IMessageService } from 'vs/platform/message/common/message';
import { ConfigWatcher } from 'vs/base/node/config';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import * as dom from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { KeybindingIO, OutputBuilder, IUserKeybindingItem } from 'vs/workbench/services/keybinding/common/keybindingIO';
import * as nativeKeymap from 'native-keymap';
import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper';
import { WindowsKeyboardMapper, IWindowsKeyboardMapping, windowsKeyboardMappingEquals } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
import { IMacLinuxKeyboardMapping, MacLinuxKeyboardMapper, macLinuxKeyboardMappingEquals } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper';
import { MacLinuxFallbackKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper';
import Event, { Emitter } from 'vs/base/common/event';
import { Extensions as ConfigExtensions, IConfigurationRegistry, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { onUnexpectedError } from 'vs/base/common/errors';
export class KeyboardMapperFactory {
public static INSTANCE = new KeyboardMapperFactory();
private _isISOKeyboard: boolean;
private _layoutInfo: nativeKeymap.IKeyboardLayoutInfo;
private _rawMapping: nativeKeymap.IKeyboardMapping;
private _keyboardMapper: IKeyboardMapper;
private _initialized: boolean;
private _onDidChangeKeyboardMapper: Emitter<void> = new Emitter<void>();
public onDidChangeKeyboardMapper: Event<void> = this._onDidChangeKeyboardMapper.event;
private constructor() {
this._isISOKeyboard = false;
this._layoutInfo = null;
this._rawMapping = null;
this._keyboardMapper = null;
this._initialized = false;
}
public _onKeyboardLayoutChanged(isISOKeyboard: boolean): void {
isISOKeyboard = !!isISOKeyboard;
if (this._initialized) {
this._setKeyboardData(isISOKeyboard, nativeKeymap.getCurrentKeyboardLayout(), nativeKeymap.getKeyMap());
} else {
this._isISOKeyboard = isISOKeyboard;
}
}
public getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper {
if (!this._initialized) {
this._setKeyboardData(this._isISOKeyboard, nativeKeymap.getCurrentKeyboardLayout(), nativeKeymap.getKeyMap());
}
if (dispatchConfig === DispatchConfig.KeyCode) {
// Forcefully set to use keyCode
return new MacLinuxFallbackKeyboardMapper(OS);
}
return this._keyboardMapper;
}
public getCurrentKeyboardLayout(): nativeKeymap.IKeyboardLayoutInfo {
if (!this._initialized) {
this._setKeyboardData(this._isISOKeyboard, nativeKeymap.getCurrentKeyboardLayout(), nativeKeymap.getKeyMap());
}
return this._layoutInfo;
}
private static _isUSStandard(_kbInfo: nativeKeymap.IKeyboardLayoutInfo): boolean {
if (OS === OperatingSystem.Linux) {
const kbInfo = <nativeKeymap.ILinuxKeyboardLayoutInfo>_kbInfo;
return (kbInfo && kbInfo.layout === 'us');
}
if (OS === OperatingSystem.Macintosh) {
const kbInfo = <nativeKeymap.IMacKeyboardLayoutInfo>_kbInfo;
return (kbInfo && kbInfo.id === 'com.apple.keylayout.US');
}
if (OS === OperatingSystem.Windows) {
const kbInfo = <nativeKeymap.IWindowsKeyboardLayoutInfo>_kbInfo;
return (kbInfo && kbInfo.name === '00000409');
}
return false;
}
public getRawKeyboardMapping(): nativeKeymap.IKeyboardMapping {
if (!this._initialized) {
this._setKeyboardData(this._isISOKeyboard, nativeKeymap.getCurrentKeyboardLayout(), nativeKeymap.getKeyMap());
}
return this._rawMapping;
}
private _setKeyboardData(isISOKeyboard: boolean, layoutInfo: nativeKeymap.IKeyboardLayoutInfo, rawMapping: nativeKeymap.IKeyboardMapping): void {
this._layoutInfo = layoutInfo;
if (this._initialized && this._isISOKeyboard === isISOKeyboard && KeyboardMapperFactory._equals(this._rawMapping, rawMapping)) {
// nothing to do...
return;
}
this._initialized = true;
this._isISOKeyboard = isISOKeyboard;
this._rawMapping = rawMapping;
this._keyboardMapper = KeyboardMapperFactory._createKeyboardMapper(this._isISOKeyboard, this._layoutInfo, this._rawMapping);
this._onDidChangeKeyboardMapper.fire();
}
private static _createKeyboardMapper(isISOKeyboard: boolean, layoutInfo: nativeKeymap.IKeyboardLayoutInfo, rawMapping: nativeKeymap.IKeyboardMapping): IKeyboardMapper {
const isUSStandard = KeyboardMapperFactory._isUSStandard(layoutInfo);
if (OS === OperatingSystem.Windows) {
return new WindowsKeyboardMapper(isUSStandard, <IWindowsKeyboardMapping>rawMapping);
}
if (Object.keys(rawMapping).length === 0) {
// Looks like reading the mappings failed (most likely Mac + Japanese/Chinese keyboard layouts)
return new MacLinuxFallbackKeyboardMapper(OS);
}
if (OS === OperatingSystem.Macintosh) {
const kbInfo = <nativeKeymap.IMacKeyboardLayoutInfo>layoutInfo;
if (kbInfo.id === 'com.apple.keylayout.DVORAK-QWERTYCMD') {
// Use keyCode based dispatching for DVORAK - QWERTY ⌘
return new MacLinuxFallbackKeyboardMapper(OS);
}
}
return new MacLinuxKeyboardMapper(isISOKeyboard, isUSStandard, <IMacLinuxKeyboardMapping>rawMapping, OS);
}
private static _equals(a: nativeKeymap.IKeyboardMapping, b: nativeKeymap.IKeyboardMapping): boolean {
if (OS === OperatingSystem.Windows) {
return windowsKeyboardMappingEquals(<IWindowsKeyboardMapping>a, <IWindowsKeyboardMapping>b);
}
return macLinuxKeyboardMappingEquals(<IMacLinuxKeyboardMapping>a, <IMacLinuxKeyboardMapping>b);
}
}
interface ContributedKeyBinding {
command: string;
key: string;
when?: string;
mac?: string;
linux?: string;
win?: string;
}
function isContributedKeyBindingsArray(thing: ContributedKeyBinding | ContributedKeyBinding[]): thing is ContributedKeyBinding[] {
return Array.isArray(thing);
}
function isValidContributedKeyBinding(keyBinding: ContributedKeyBinding, rejects: string[]): boolean {
if (!keyBinding) {
rejects.push(nls.localize('nonempty', "expected non-empty value."));
return false;
}
if (typeof keyBinding.command !== 'string') {
rejects.push(nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
return false;
}
if (typeof keyBinding.key !== 'string') {
rejects.push(nls.localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'key'));
return false;
}
if (keyBinding.when && typeof keyBinding.when !== 'string') {
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));
return false;
}
if (keyBinding.mac && typeof keyBinding.mac !== 'string') {
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'mac'));
return false;
}
if (keyBinding.linux && typeof keyBinding.linux !== 'string') {
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'linux'));
return false;
}
if (keyBinding.win && typeof keyBinding.win !== 'string') {
rejects.push(nls.localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'win'));
return false;
}
return true;
}
let keybindingType: IJSONSchema = {
type: 'object',
default: { command: '', key: '' },
properties: {
command: {
description: nls.localize('vscode.extension.contributes.keybindings.command', 'Identifier of the command to run when keybinding is triggered.'),
type: 'string'
},
key: {
description: nls.localize('vscode.extension.contributes.keybindings.key', 'Key or key sequence (separate keys with plus-sign and sequences with space, e.g Ctrl+O and Ctrl+L L for a chord).'),
type: 'string'
},
mac: {
description: nls.localize('vscode.extension.contributes.keybindings.mac', 'Mac specific key or key sequence.'),
type: 'string'
},
linux: {
description: nls.localize('vscode.extension.contributes.keybindings.linux', 'Linux specific key or key sequence.'),
type: 'string'
},
win: {
description: nls.localize('vscode.extension.contributes.keybindings.win', 'Windows specific key or key sequence.'),
type: 'string'
},
when: {
description: nls.localize('vscode.extension.contributes.keybindings.when', 'Condition when the key is active.'),
type: 'string'
}
}
};
let keybindingsExtPoint = ExtensionsRegistry.registerExtensionPoint<ContributedKeyBinding | ContributedKeyBinding[]>('keybindings', [], {
description: nls.localize('vscode.extension.contributes.keybindings', "Contributes keybindings."),
oneOf: [
keybindingType,
{
type: 'array',
items: keybindingType
}
]
});
export const enum DispatchConfig {
Code,
KeyCode
}
function getDispatchConfig(configurationService: IConfigurationService): DispatchConfig {
const keyboard = configurationService.getConfiguration('keyboard');
const r = (keyboard ? (<any>keyboard).dispatch : null);
return (r === 'keyCode' ? DispatchConfig.KeyCode : DispatchConfig.Code);
}
export class WorkbenchKeybindingService extends AbstractKeybindingService {
private _keyboardMapper: IKeyboardMapper;
private _cachedResolver: KeybindingResolver;
private _firstTimeComputingResolver: boolean;
private userKeybindings: ConfigWatcher<IUserFriendlyKeybinding[]>;
constructor(
windowElement: Window,
@IContextKeyService contextKeyService: IContextKeyService,
@ICommandService commandService: ICommandService,
@ITelemetryService private telemetryService: ITelemetryService,
@IMessageService messageService: IMessageService,
@IEnvironmentService environmentService: IEnvironmentService,
@IStatusbarService statusBarService: IStatusbarService,
@IConfigurationService configurationService: IConfigurationService
) {
super(contextKeyService, commandService, messageService, statusBarService);
let dispatchConfig = getDispatchConfig(configurationService);
configurationService.onDidUpdateConfiguration((e) => {
let newDispatchConfig = getDispatchConfig(configurationService);
if (dispatchConfig === newDispatchConfig) {
return;
}
dispatchConfig = newDispatchConfig;
this._keyboardMapper = KeyboardMapperFactory.INSTANCE.getKeyboardMapper(dispatchConfig);
this.updateResolver({ source: KeybindingSource.Default });
});
this._keyboardMapper = KeyboardMapperFactory.INSTANCE.getKeyboardMapper(dispatchConfig);
KeyboardMapperFactory.INSTANCE.onDidChangeKeyboardMapper(() => {
this._keyboardMapper = KeyboardMapperFactory.INSTANCE.getKeyboardMapper(dispatchConfig);
this.updateResolver({ source: KeybindingSource.Default });
});
this._cachedResolver = null;
this._firstTimeComputingResolver = true;
this.userKeybindings = new ConfigWatcher(environmentService.appKeybindingsPath, { defaultConfig: [], onError: error => onUnexpectedError(error) });
this.toDispose.push(toDisposable(() => this.userKeybindings.dispose()));
keybindingsExtPoint.setHandler((extensions) => {
let commandAdded = false;
for (let extension of extensions) {
commandAdded = this._handleKeybindingsExtensionPointUser(extension.description.isBuiltin, extension.value, extension.collector) || commandAdded;
}
if (commandAdded) {
this.updateResolver({ source: KeybindingSource.Default });
}
});
this.toDispose.push(this.userKeybindings.onDidUpdateConfiguration(event => this.updateResolver({
source: KeybindingSource.User,
keybindings: event.config
})));
this.toDispose.push(dom.addDisposableListener(windowElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let keyEvent = new StandardKeyboardEvent(e);
let shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
if (shouldPreventDefault) {
keyEvent.preventDefault();
}
}));
keybindingsTelemetry(telemetryService, this);
let data = KeyboardMapperFactory.INSTANCE.getCurrentKeyboardLayout();
telemetryService.publicLog('keyboardLayout', {
currentKeyboardLayout: data
});
}
public dumpDebugInfo(): string {
const layoutInfo = JSON.stringify(KeyboardMapperFactory.INSTANCE.getCurrentKeyboardLayout(), null, '\t');
const mapperInfo = this._keyboardMapper.dumpDebugInfo();
const rawMapping = JSON.stringify(KeyboardMapperFactory.INSTANCE.getRawKeyboardMapping(), null, '\t');
return `Layout info:\n${layoutInfo}\n${mapperInfo}\n\nRaw mapping:\n${rawMapping}`;
}
private _safeGetConfig(): IUserFriendlyKeybinding[] {
let rawConfig = this.userKeybindings.getConfig();
if (Array.isArray(rawConfig)) {
return rawConfig;
}
return [];
}
public customKeybindingsCount(): number {
let userKeybindings = this._safeGetConfig();
return userKeybindings.length;
}
private updateResolver(event: IKeybindingEvent): void {
this._cachedResolver = null;
this._onDidUpdateKeybindings.fire(event);
}
protected _getResolver(): KeybindingResolver {
if (!this._cachedResolver) {
const defaults = this._resolveKeybindingItems(KeybindingsRegistry.getDefaultKeybindings(), true);
const overrides = this._resolveUserKeybindingItems(this._getExtraKeybindings(this._firstTimeComputingResolver), false);
this._cachedResolver = new KeybindingResolver(defaults, overrides);
this._firstTimeComputingResolver = false;
}
return this._cachedResolver;
}
private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
let result: ResolvedKeybindingItem[] = [], resultLen = 0;
for (let i = 0, len = items.length; i < len; i++) {
const item = items[i];
const when = (item.when ? item.when.normalize() : null);
const keybinding = item.keybinding;
if (!keybinding) {
// This might be a removal keybinding item in user settings => accept it
result[resultLen++] = new ResolvedKeybindingItem(null, item.command, item.commandArgs, when, isDefault);
} else {
const resolvedKeybindings = this.resolveKeybinding(keybinding);
for (let j = 0; j < resolvedKeybindings.length; j++) {
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybindings[j], item.command, item.commandArgs, when, isDefault);
}
}
}
return result;
}
private _resolveUserKeybindingItems(items: IUserKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
let result: ResolvedKeybindingItem[] = [], resultLen = 0;
for (let i = 0, len = items.length; i < len; i++) {
const item = items[i];
const when = (item.when ? item.when.normalize() : null);
const firstPart = item.firstPart;
const chordPart = item.chordPart;
if (!firstPart) {
// This might be a removal keybinding item in user settings => accept it
result[resultLen++] = new ResolvedKeybindingItem(null, item.command, item.commandArgs, when, isDefault);
} else {
const resolvedKeybindings = this._keyboardMapper.resolveUserBinding(firstPart, chordPart);
for (let j = 0; j < resolvedKeybindings.length; j++) {
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybindings[j], item.command, item.commandArgs, when, isDefault);
}
}
}
return result;
}
private _getExtraKeybindings(isFirstTime: boolean): IUserKeybindingItem[] {
let extraUserKeybindings: IUserFriendlyKeybinding[] = this._safeGetConfig();
if (!isFirstTime) {
let cnt = extraUserKeybindings.length;
this.telemetryService.publicLog('customKeybindingsChanged', {
keyCount: cnt
});
}
return extraUserKeybindings.map((k) => KeybindingIO.readUserKeybindingItem(k, OS));
}
public resolveKeybinding(kb: Keybinding): ResolvedKeybinding[] {
return this._keyboardMapper.resolveKeybinding(kb);
}
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
return this._keyboardMapper.resolveKeyboardEvent(keyboardEvent);
}
public resolveUserBinding(userBinding: string): ResolvedKeybinding[] {
const [firstPart, chordPart] = KeybindingIO._readUserBinding(userBinding);
return this._keyboardMapper.resolveUserBinding(firstPart, chordPart);
}
private _handleKeybindingsExtensionPointUser(isBuiltin: boolean, keybindings: ContributedKeyBinding | ContributedKeyBinding[], collector: ExtensionMessageCollector): boolean {
if (isContributedKeyBindingsArray(keybindings)) {
let commandAdded = false;
for (let i = 0, len = keybindings.length; i < len; i++) {
commandAdded = this._handleKeybinding(isBuiltin, i + 1, keybindings[i], collector) || commandAdded;
}
return commandAdded;
} else {
return this._handleKeybinding(isBuiltin, 1, keybindings, collector);
}
}
private _handleKeybinding(isBuiltin: boolean, idx: number, keybindings: ContributedKeyBinding, collector: ExtensionMessageCollector): boolean {
let rejects: string[] = [];
let commandAdded = false;
if (isValidContributedKeyBinding(keybindings, rejects)) {
let rule = this._asCommandRule(isBuiltin, idx++, keybindings);
if (rule) {
KeybindingsRegistry.registerKeybindingRule2(rule);
commandAdded = true;
}
}
if (rejects.length > 0) {
collector.error(nls.localize(
'invalid.keybindings',
"Invalid `contributes.{0}`: {1}",
keybindingsExtPoint.name,
rejects.join('\n')
));
}
return commandAdded;
}
private _asCommandRule(isBuiltin: boolean, idx: number, binding: ContributedKeyBinding): IKeybindingRule2 {
let { command, when, key, mac, linux, win } = binding;
let weight: number;
if (isBuiltin) {
weight = KeybindingsRegistry.WEIGHT.builtinExtension(idx);
} else {
weight = KeybindingsRegistry.WEIGHT.externalExtension(idx);
}
let desc = {
id: command,
when: ContextKeyExpr.deserialize(when),
weight: weight,
primary: KeybindingIO.readKeybinding(key, OS),
mac: mac && { primary: KeybindingIO.readKeybinding(mac, OS) },
linux: linux && { primary: KeybindingIO.readKeybinding(linux, OS) },
win: win && { primary: KeybindingIO.readKeybinding(win, OS) }
};
if (!desc.primary && !desc.mac && !desc.linux && !desc.win) {
return undefined;
}
return desc;
}
public getDefaultKeybindingsContent(): string {
const resolver = this._getResolver();
const defaultKeybindings = resolver.getDefaultKeybindings();
const boundCommands = resolver.getDefaultBoundCommands();
return (
WorkbenchKeybindingService._getDefaultKeybindings(defaultKeybindings)
+ '\n\n'
+ WorkbenchKeybindingService._getAllCommandsAsComment(boundCommands)
);
}
private static _getDefaultKeybindings(defaultKeybindings: ResolvedKeybindingItem[]): string {
let out = new OutputBuilder();
out.writeLine('[');
let lastIndex = defaultKeybindings.length - 1;
defaultKeybindings.forEach((k, index) => {
KeybindingIO.writeKeybindingItem(out, k, OS);
if (index !== lastIndex) {
out.writeLine(',');
} else {
out.writeLine();
}
});
out.writeLine(']');
return out.toString();
}
private static _getAllCommandsAsComment(boundCommands: Map<string, boolean>): string {
const unboundCommands = KeybindingResolver.getAllUnboundCommands(boundCommands);
let pretty = unboundCommands.sort().join('\n// - ');
return '// ' + nls.localize('unboundCommands', "Here are other available commands: ") + '\n// - ' + pretty;
}
}
let schemaId = 'vscode://schemas/keybindings';
let schema: IJSONSchema = {
'id': schemaId,
'type': 'array',
'title': nls.localize('keybindings.json.title', "Keybindings configuration"),
'items': {
'required': ['key'],
'type': 'object',
'defaultSnippets': [{ 'body': { 'key': '$1', 'command': '$2', 'when': '$3' } }],
'properties': {
'key': {
'type': 'string',
'description': nls.localize('keybindings.json.key', "Key or key sequence (separated by space)"),
},
'command': {
'description': nls.localize('keybindings.json.command', "Name of the command to execute"),
},
'when': {
'type': 'string',
'description': nls.localize('keybindings.json.when', "Condition when the key is active.")
},
'args': {
'description': nls.localize('keybindings.json.args', "Arguments to pass to the command to execute.")
}
}
}
};
let schemaRegistry = <IJSONContributionRegistry>Registry.as(Extensions.JSONContribution);
schemaRegistry.registerSchema(schemaId, schema);
if (OS === OperatingSystem.Macintosh || OS === OperatingSystem.Linux) {
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
const keyboardConfiguration: IConfigurationNode = {
'id': 'keyboard',
'order': 15,
'type': 'object',
'title': nls.localize('keyboardConfigurationTitle', "Keyboard"),
'overridable': true,
'properties': {
'keyboard.dispatch': {
'type': 'string',
'enum': ['code', 'keyCode'],
'default': 'code',
'description': nls.localize('dispatch', "Controls the dispatching logic for key presses to use either `keydown.code` (recommended) or `keydown.keyCode`.")
}
}
};
configurationRegistry.registerConfiguration(keyboardConfiguration);
}

View File

@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import Severity from 'vs/base/common/severity';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ILifecycleService, ShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, handleVetos } from 'vs/platform/lifecycle/common/lifecycle';
import { IMessageService } from 'vs/platform/message/common/message';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ipcRenderer as ipc } from 'electron';
import Event, { Emitter } from 'vs/base/common/event';
import { IWindowService } from 'vs/platform/windows/common/windows';
export class LifecycleService implements ILifecycleService {
private static readonly _lastShutdownReasonKey = 'lifecyle.lastShutdownReason';
public _serviceBrand: any;
private readonly _onDidChangePhase = new Emitter<LifecyclePhase>();
private readonly _onWillShutdown = new Emitter<ShutdownEvent>();
private readonly _onShutdown = new Emitter<ShutdownReason>();
private readonly _startupKind: StartupKind;
private _phase: LifecyclePhase = LifecyclePhase.Starting;
constructor(
@IMessageService private _messageService: IMessageService,
@IWindowService private _windowService: IWindowService,
@IStorageService private _storageService: IStorageService
) {
this._registerListeners();
const lastShutdownReason = this._storageService.getInteger(LifecycleService._lastShutdownReasonKey, StorageScope.WORKSPACE);
this._storageService.remove(LifecycleService._lastShutdownReasonKey, StorageScope.WORKSPACE);
if (lastShutdownReason === ShutdownReason.RELOAD) {
this._startupKind = StartupKind.ReloadedWindow;
} else if (lastShutdownReason === ShutdownReason.LOAD) {
this._startupKind = StartupKind.ReopenedWindow;
} else {
this._startupKind = StartupKind.NewWindow;
}
}
public get phase(): LifecyclePhase {
return this._phase;
}
public set phase(value: LifecyclePhase) {
if (this._phase !== value) {
this._phase = value;
this._onDidChangePhase.fire(value);
}
}
public get startupKind(): StartupKind {
return this._startupKind;
}
public get onDidChangePhase(): Event<LifecyclePhase> {
return this._onDidChangePhase.event;
}
public get onWillShutdown(): Event<ShutdownEvent> {
return this._onWillShutdown.event;
}
public get onShutdown(): Event<ShutdownReason> {
return this._onShutdown.event;
}
private _registerListeners(): void {
const windowId = this._windowService.getCurrentWindowId();
// Main side indicates that window is about to unload, check for vetos
ipc.on('vscode:beforeUnload', (event, reply: { okChannel: string, cancelChannel: string, reason: ShutdownReason, payload: object }) => {
this.phase = LifecyclePhase.ShuttingDown;
this._storageService.store(LifecycleService._lastShutdownReasonKey, JSON.stringify(reply.reason), StorageScope.WORKSPACE);
// trigger onWillShutdown events and veto collecting
this.onBeforeUnload(reply.reason, reply.payload).done(veto => {
if (veto) {
this._storageService.remove(LifecycleService._lastShutdownReasonKey, StorageScope.WORKSPACE);
this.phase = LifecyclePhase.Running; // reset this flag since the shutdown has been vetoed!
ipc.send(reply.cancelChannel, windowId);
} else {
this._onShutdown.fire(reply.reason);
ipc.send(reply.okChannel, windowId);
}
});
});
}
private onBeforeUnload(reason: ShutdownReason, payload?: object): TPromise<boolean> {
const vetos: (boolean | TPromise<boolean>)[] = [];
this._onWillShutdown.fire({
veto(value) {
vetos.push(value);
},
reason,
payload
});
return handleVetos(vetos, err => this._messageService.show(Severity.Error, toErrorMessage(err)));
}
}

View File

@@ -0,0 +1,117 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.global-message-list {
font-size: 12px;
position: absolute;
z-index: 300;
list-style-type: none;
line-height: 35px;
margin: 0;
padding: 0;
width: 70%;
left: 15%;
}
.hc-black .global-message-list {
outline: 2px solid;
outline-offset: -2px;
box-shadow: none;
}
.global-message-list.transition {
-webkit-transition: top 200ms linear;
-ms-transition: top 200ms linear;
-moz-transition: top 200ms linear;
-khtml-transition: top 200ms linear;
-o-transition: top 200ms linear;
transition: top 200ms linear;
}
.global-message-list ul.message-list {
margin: 0;
padding: 0;
}
.global-message-list li.message-list-entry {
padding-left: 10px;
padding-right: 10px;
overflow: hidden;
-webkit-box-sizing: border-box;
-o-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box; /* Important so that borders added don't add to the overal size */
height: 35px;
}
.global-message-list li.message-list-entry-with-action {
padding-right: 0;
}
.global-message-list li.message-list-entry .message-left-side {
-webkit-user-select: text;
-ms-user-select: text;
-khtml-user-select: text;
-moz-user-select: text;
-o-user-select: text;
user-select: text;
}
.global-message-list li.message-list-entry .message-left-side.severity {
padding: 2px 4px;
margin-right: 10px;
font-weight: bold;
font-size: 11px;
-webkit-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
-moz-user-select: -moz-none;
-o-user-select: none;
user-select: none;
}
.global-message-list li.message-list-entry .message-left-side.message-overflow-ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.global-message-list li.message-list-entry .actions-container {
float: right;
}
.global-message-list li.message-list-entry .actions-container .message-action {
display: inline-block;
}
.global-message-list li.message-list-entry .actions-container .message-action .action-button {
padding-left: 10px;
padding-right: 10px;
display: inline-block;
color: inherit;
text-decoration: none;
cursor: pointer;
}
.hc-black .global-message-list li.message-list-entry .message-left-side.severity {
border: 1px solid;
}
.hc-black .global-message-list li.message-list-entry .actions-container .message-action .action-button {
border: 1px solid;
}
.hc-black .global-message-list li.message-list-entry .actions-container .message-action .action-button:focus {
outline-offset: -4px; /* Helps high-contrast outline avoid clipping. */
}
/* TODO@theme */
.vs .global-message-list li.message-list-entry .actions-container .message-action .action-button:focus,
.vs-dark .global-message-list li.message-list-entry .actions-container .message-action .action-button:focus {
outline-color: rgba(255, 255, 255, .5); /* buttons have a blue color, so focus indication needs to be different */
}

View File

@@ -0,0 +1,490 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/messageList';
import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import { Builder, $ } from 'vs/base/browser/builder';
import DOM = require('vs/base/browser/dom');
import * as browser from 'vs/base/browser/browser';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import aria = require('vs/base/browser/ui/aria/aria');
import types = require('vs/base/common/types');
import Event, { Emitter } from 'vs/base/common/event';
import { Action } from 'vs/base/common/actions';
import htmlRenderer = require('vs/base/browser/htmlContentRenderer');
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { NOTIFICATIONS_FOREGROUND, NOTIFICATIONS_BACKGROUND, NOTIFICATIONS_BUTTON_BACKGROUND, NOTIFICATIONS_BUTTON_HOVER_BACKGROUND, NOTIFICATIONS_BUTTON_FOREGROUND, NOTIFICATIONS_INFO_BACKGROUND, NOTIFICATIONS_WARNING_BACKGROUND, NOTIFICATIONS_ERROR_BACKGROUND, NOTIFICATIONS_INFO_FOREGROUND, NOTIFICATIONS_WARNING_FOREGROUND, NOTIFICATIONS_ERROR_FOREGROUND } from 'vs/workbench/common/theme';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
export enum Severity {
Info,
Warning,
Error
}
export interface IMessageWithAction {
message: string;
actions: Action[];
source: string;
}
interface IMessageEntry {
id: any;
text: string;
source: string;
severity: Severity;
time: number;
count?: number;
actions: Action[];
onHide: () => void;
}
export class IMessageListOptions {
purgeInterval: number;
maxMessages: number;
maxMessageLength: number;
}
const DEFAULT_MESSAGE_LIST_OPTIONS = {
purgeInterval: 10000,
maxMessages: 5,
maxMessageLength: 500
};
export class MessageList {
private messages: IMessageEntry[];
private messageListPurger: TPromise<void>;
private messageListContainer: Builder;
private container: HTMLElement;
private options: IMessageListOptions;
private _onMessagesShowing: Emitter<void>;
private _onMessagesCleared: Emitter<void>;
private toDispose: IDisposable[];
private background = Color.fromHex('#333333');
private foreground = Color.fromHex('#EEEEEE');
private widgetShadow = Color.fromHex('#000000');
private outlineBorder: Color;
private buttonBackground = Color.fromHex('#0E639C');
private buttonForeground = this.foreground;
private infoBackground = Color.fromHex('#007ACC');
private infoForeground = this.foreground;
private warningBackground = Color.fromHex('#B89500');
private warningForeground = this.foreground;
private errorBackground = Color.fromHex('#BE1100');
private errorForeground = this.foreground;
constructor(
container: HTMLElement,
private telemetryService: ITelemetryService,
options: IMessageListOptions = DEFAULT_MESSAGE_LIST_OPTIONS
) {
this.toDispose = [];
this.messages = [];
this.messageListPurger = null;
this.container = container;
this.options = options;
this._onMessagesShowing = new Emitter<void>();
this._onMessagesCleared = new Emitter<void>();
this.registerListeners();
}
private registerListeners(): void {
this.toDispose.push(browser.onDidChangeFullscreen(() => this.positionMessageList()));
this.toDispose.push(browser.onDidChangeZoomLevel(() => this.positionMessageList()));
this.toDispose.push(registerThemingParticipant((theme, collector) => {
this.background = theme.getColor(NOTIFICATIONS_BACKGROUND);
this.foreground = theme.getColor(NOTIFICATIONS_FOREGROUND);
this.widgetShadow = theme.getColor(widgetShadow);
this.outlineBorder = theme.getColor(contrastBorder);
this.buttonBackground = theme.getColor(NOTIFICATIONS_BUTTON_BACKGROUND);
this.buttonForeground = theme.getColor(NOTIFICATIONS_BUTTON_FOREGROUND);
this.infoBackground = theme.getColor(NOTIFICATIONS_INFO_BACKGROUND);
this.infoForeground = theme.getColor(NOTIFICATIONS_INFO_FOREGROUND);
this.warningBackground = theme.getColor(NOTIFICATIONS_WARNING_BACKGROUND);
this.warningForeground = theme.getColor(NOTIFICATIONS_WARNING_FOREGROUND);
this.errorBackground = theme.getColor(NOTIFICATIONS_ERROR_BACKGROUND);
this.errorForeground = theme.getColor(NOTIFICATIONS_ERROR_FOREGROUND);
const buttonHoverBackgroundColor = theme.getColor(NOTIFICATIONS_BUTTON_HOVER_BACKGROUND);
if (buttonHoverBackgroundColor) {
collector.addRule(`.global-message-list li.message-list-entry .actions-container .message-action .action-button:hover { background-color: ${buttonHoverBackgroundColor} !important; }`);
}
this.updateStyles();
}));
}
public get onMessagesShowing(): Event<void> {
return this._onMessagesShowing.event;
}
public get onMessagesCleared(): Event<void> {
return this._onMessagesCleared.event;
}
public updateStyles(): void {
if (this.messageListContainer) {
this.messageListContainer.style('background-color', this.background ? this.background.toString() : null);
this.messageListContainer.style('color', this.foreground ? this.foreground.toString() : null);
this.messageListContainer.style('outline-color', this.outlineBorder ? this.outlineBorder.toString() : null);
this.messageListContainer.style('box-shadow', this.widgetShadow ? `0 5px 8px ${this.widgetShadow}` : null);
}
}
public showMessage(severity: Severity, message: string, onHide?: () => void): () => void;
public showMessage(severity: Severity, message: Error, onHide?: () => void): () => void;
public showMessage(severity: Severity, message: string[], onHide?: () => void): () => void;
public showMessage(severity: Severity, message: Error[], onHide?: () => void): () => void;
public showMessage(severity: Severity, message: IMessageWithAction, onHide?: () => void): () => void;
public showMessage(severity: Severity, message: any, onHide?: () => void): () => void {
if (Array.isArray(message)) {
const closeFns: Function[] = [];
message.forEach((msg: any) => closeFns.push(this.showMessage(severity, msg, onHide)));
return () => closeFns.forEach((fn) => fn());
}
// Return only if we are unable to extract a message text
const messageText = this.getMessageText(message);
if (!messageText || typeof messageText !== 'string') {
return () => {/* empty */ };
}
// Show message
return this.doShowMessage(message, messageText, severity, onHide);
}
private getMessageText(message: any): string {
if (types.isString(message)) {
return message;
}
if (message instanceof Error) {
return toErrorMessage(message, false);
}
if (message && (<IMessageWithAction>message).message) {
return (<IMessageWithAction>message).message;
}
return null;
}
private doShowMessage(id: string, message: string, severity: Severity, onHide: () => void): () => void;
private doShowMessage(id: Error, message: string, severity: Severity, onHide: () => void): () => void;
private doShowMessage(id: IMessageWithAction, message: string, severity: Severity, onHide: () => void): () => void;
private doShowMessage(id: any, message: string, severity: Severity, onHide: () => void): () => void {
// Trigger Auto-Purge of messages to keep list small
this.purgeMessages();
// Store in Memory (new messages come first so that they show up on top)
this.messages.unshift({
id: id,
text: message,
severity: severity,
time: Date.now(),
actions: (<IMessageWithAction>id).actions,
source: (<IMessageWithAction>id).source,
onHide
});
// Render
this.renderMessages(true, 1);
// Support in Screen Readers too
let alertText: string;
if (severity === Severity.Error) {
alertText = nls.localize('alertErrorMessage', "Error: {0}", message);
} else if (severity === Severity.Warning) {
alertText = nls.localize('alertWarningMessage', "Warning: {0}", message);
} else {
alertText = nls.localize('alertInfoMessage', "Info: {0}", message);
}
aria.alert(alertText);
return () => this.hideMessage(id);
}
private renderMessages(animate: boolean, delta: number): void {
const container = $(this.container);
// Lazily create, otherwise clear old
if (!this.messageListContainer) {
this.messageListContainer = $('.global-message-list').appendTo(container);
} else {
$(this.messageListContainer).empty();
$(this.messageListContainer).removeClass('transition');
}
// Support animation for messages by moving the container out of view and then in
if (animate) {
$(this.messageListContainer).style('top', '-35px');
}
// Render Messages as List Items
$(this.messageListContainer).ul({ 'class': 'message-list' }, ul => {
const messages = this.prepareMessages();
if (messages.length > 0) {
this._onMessagesShowing.fire();
} else {
this._onMessagesCleared.fire();
}
messages.forEach((message: IMessageEntry, index: number) => {
this.renderMessage(message, $(ul), messages.length, delta);
});
// Support animation for messages by moving the container out of view and then in
if (animate) {
setTimeout(() => {
this.positionMessageList();
$(this.messageListContainer).addClass('transition');
}, 50 /* Need this delay to reliably get the animation on some browsers */);
}
});
// Styles
this.updateStyles();
}
private positionMessageList(animate?: boolean): void {
if (!this.messageListContainer) {
return; // not yet created
}
$(this.messageListContainer).removeClass('transition'); // disable any animations
let position = 0;
if (!browser.isFullscreen() && DOM.hasClass(this.container, 'titlebar-style-custom')) {
position = 22 / browser.getZoomFactor(); // adjust the position based on title bar size and zoom factor
}
$(this.messageListContainer).style('top', `${position}px`);
}
private renderMessage(message: IMessageEntry, container: Builder, total: number, delta: number): void {
container.li({ class: 'message-list-entry message-list-entry-with-action' }, li => {
// Actions (if none provided, add one default action to hide message)
const messageActions = this.getMessageActions(message);
li.div({ class: 'actions-container' }, actionContainer => {
for (let i = 0; i < messageActions.length; i++) {
const action = messageActions[i];
actionContainer.div({ class: 'message-action' }, div => {
div.a({ class: 'action-button', tabindex: '0', role: 'button' })
.style('border-color', this.outlineBorder ? this.outlineBorder.toString() : null)
.style('background-color', this.buttonBackground ? this.buttonBackground.toString() : null)
.style('color', this.buttonForeground ? this.buttonForeground.toString() : null)
.text(action.label)
.on([DOM.EventType.CLICK, DOM.EventType.KEY_DOWN], e => {
if (e instanceof KeyboardEvent) {
const event = new StandardKeyboardEvent(e);
if (!event.equals(KeyCode.Enter) && !event.equals(KeyCode.Space)) {
return; // Only handle Enter/Escape for keyboard access
}
}
DOM.EventHelper.stop(e, true);
this.telemetryService.publicLog('workbenchActionExecuted', { id: action.id, from: 'message' });
(action.run() || TPromise.as(null))
.then(null, error => this.showMessage(Severity.Error, error))
.done(r => {
if (typeof r === 'boolean' && r === false) {
return;
}
this.hideMessage(message.text); // hide all matching the text since there may be duplicates
});
});
});
}
});
// Text
const text = message.text.substr(0, this.options.maxMessageLength);
li.div({ class: 'message-left-side' }, div => {
div.addClass('message-overflow-ellipsis');
// Severity indicator
const sev = message.severity;
const label = (sev === Severity.Error) ? nls.localize('error', "Error") : (sev === Severity.Warning) ? nls.localize('warning', "Warn") : nls.localize('info', "Info");
const color = (sev === Severity.Error) ? this.errorBackground : (sev === Severity.Warning) ? this.warningBackground : this.infoBackground;
const foregroundColor = (sev === Severity.Error) ? this.errorForeground : (sev === Severity.Warning) ? this.warningForeground : this.infoForeground;
const sevLabel = $().span({ class: `message-left-side severity ${sev === Severity.Error ? 'app-error' : sev === Severity.Warning ? 'app-warning' : 'app-info'}`, text: label });
sevLabel.style('border-color', this.outlineBorder ? this.outlineBorder.toString() : null);
sevLabel.style('background-color', color ? color.toString() : null);
sevLabel.style('color', foregroundColor ? foregroundColor.toString() : null);
sevLabel.appendTo(div);
// Error message
const messageContentElement = htmlRenderer.renderFormattedText(text, {
inline: true,
className: 'message-left-side',
});
// Hover title
const title = message.source ? `[${message.source}] ${messageContentElement.textContent}` : messageContentElement.textContent;
sevLabel.title(title);
$(messageContentElement as HTMLElement).title(title).appendTo(div);
});
});
}
private getMessageActions(message: IMessageEntry): Action[] {
let messageActions: Action[];
if (message.actions && message.actions.length > 0) {
messageActions = message.actions;
} else {
messageActions = [
new Action('close.message.action', nls.localize('close', "Close"), null, true, () => {
this.hideMessage(message.text); // hide all matching the text since there may be duplicates
return TPromise.as(true);
})
];
}
return messageActions;
}
private prepareMessages(): IMessageEntry[] {
// Aggregate Messages by text to reduce their count
const messages: IMessageEntry[] = [];
const handledMessages: { [message: string]: number; } = {};
let offset = 0;
for (let i = 0; i < this.messages.length; i++) {
const message = this.messages[i];
if (types.isUndefinedOrNull(handledMessages[message.text])) {
message.count = 1;
messages.push(message);
handledMessages[message.text] = offset++;
} else {
messages[handledMessages[message.text]].count++;
}
}
if (messages.length > this.options.maxMessages) {
return messages.splice(messages.length - this.options.maxMessages, messages.length);
}
return messages;
}
private disposeMessages(messages: IMessageEntry[]): void {
messages.forEach(message => {
if (message.onHide) {
message.onHide();
}
if (message.actions) {
message.actions.forEach(action => {
action.dispose();
});
}
});
}
public hideMessages(): void {
this.hideMessage();
}
public show(): void {
if (this.messageListContainer && this.messageListContainer.isHidden()) {
this.messageListContainer.show();
}
}
public hide(): void {
if (this.messageListContainer && !this.messageListContainer.isHidden()) {
this.messageListContainer.hide();
}
}
private hideMessage(messageText?: string): void;
private hideMessage(messageObj?: any): void {
let messageFound = false;
for (let i = 0; i < this.messages.length; i++) {
const message = this.messages[i];
let hide = false;
// Hide specific message
if (messageObj) {
hide = ((types.isString(messageObj) && message.text === messageObj) || message.id === messageObj);
}
// Hide all messages
else {
hide = true;
}
if (hide) {
this.disposeMessages(this.messages.splice(i, 1));
i--;
messageFound = true;
}
}
if (messageFound) {
this.renderMessages(false, -1);
}
}
private purgeMessages(): void {
// Cancel previous
if (this.messageListPurger) {
this.messageListPurger.cancel();
}
// Configure
this.messageListPurger = TPromise.timeout(this.options.purgeInterval).then(() => {
let needsUpdate = false;
let counter = 0;
for (let i = 0; i < this.messages.length; i++) {
const message = this.messages[i];
// Only purge infos and warnings and only if they are not providing actions
if (message.severity !== Severity.Error && !message.actions) {
this.disposeMessages(this.messages.splice(i, 1));
counter--;
i--;
needsUpdate = true;
}
}
if (needsUpdate) {
this.renderMessages(false, counter);
}
});
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import errors = require('vs/base/common/errors');
import { toErrorMessage } from 'vs/base/common/errorMessage';
import types = require('vs/base/common/types');
import { MessageList, Severity as BaseSeverity } from 'vs/workbench/services/message/browser/messageList';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IMessageService, IMessageWithAction, IConfirmation, Severity } from 'vs/platform/message/common/message';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import Event from 'vs/base/common/event';
interface IBufferedMessage {
severity: Severity;
message: any;
onHide: () => void;
disposeFn: () => void;
}
export class WorkbenchMessageService implements IMessageService {
public _serviceBrand: any;
private handler: MessageList;
private toDispose: IDisposable[];
private canShowMessages: boolean;
private messageBuffer: IBufferedMessage[];
constructor(
container: HTMLElement,
telemetryService: ITelemetryService
) {
this.handler = new MessageList(container, telemetryService);
this.messageBuffer = [];
this.canShowMessages = true;
this.toDispose = [this.handler];
}
public get onMessagesShowing(): Event<void> {
return this.handler.onMessagesShowing;
}
public get onMessagesCleared(): Event<void> {
return this.handler.onMessagesCleared;
}
public suspend(): void {
this.canShowMessages = false;
this.handler.hide();
}
public resume(): void {
this.canShowMessages = true;
this.handler.show();
// Release messages from buffer
while (this.messageBuffer.length) {
const bufferedMessage = this.messageBuffer.pop();
bufferedMessage.disposeFn = this.show(bufferedMessage.severity, bufferedMessage.message, bufferedMessage.onHide);
}
}
private toBaseSeverity(severity: Severity): BaseSeverity {
switch (severity) {
case Severity.Info:
return BaseSeverity.Info;
case Severity.Warning:
return BaseSeverity.Warning;
}
return BaseSeverity.Error;
}
public show(sev: Severity, message: string, onHide?: () => void): () => void;
public show(sev: Severity, message: Error, onHide?: () => void): () => void;
public show(sev: Severity, message: string[], onHide?: () => void): () => void;
public show(sev: Severity, message: Error[], onHide?: () => void): () => void;
public show(sev: Severity, message: IMessageWithAction, onHide?: () => void): () => void;
public show(sev: Severity, message: any, onHide?: () => void): () => void {
if (!message) {
return () => void 0; // guard against undefined messages
}
if (Array.isArray(message)) {
let closeFns: Function[] = [];
message.forEach((msg: any) => closeFns.push(this.show(sev, msg, onHide)));
return () => closeFns.forEach((fn) => fn());
}
if (errors.isPromiseCanceledError(message)) {
return () => void 0; // this kind of error should not be shown
}
if (types.isNumber(message.severity)) {
sev = message.severity;
}
return this.doShow(sev, message, onHide);
}
private doShow(sev: Severity, message: any, onHide?: () => void): () => void {
// Check flag if we can show a message now
if (!this.canShowMessages) {
const messageObj: IBufferedMessage = {
severity: sev,
message,
onHide,
disposeFn: () => this.messageBuffer.splice(this.messageBuffer.indexOf(messageObj), 1)
};
this.messageBuffer.push(messageObj);
// Return function that allows to remove message from buffer
return () => messageObj.disposeFn();
}
// Show in Console
if (sev === Severity.Error) {
console.error(toErrorMessage(message, true));
}
// Show in Global Handler
return this.handler.showMessage(this.toBaseSeverity(sev), message, onHide);
}
public hideAll(): void {
if (this.handler) {
this.handler.hideMessages();
}
}
public confirm(confirmation: IConfirmation): boolean {
let messageText = confirmation.message;
if (confirmation.detail) {
messageText = messageText + '\n\n' + confirmation.detail;
}
return window.confirm(messageText);
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
}

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import product from 'vs/platform/node/product';
import { TPromise } from 'vs/base/common/winjs.base';
import { WorkbenchMessageService } from 'vs/workbench/services/message/browser/messageService';
import { IConfirmation, Severity, IChoiceService } from 'vs/platform/message/common/message';
import { isLinux } from 'vs/base/common/platform';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { Action } from 'vs/base/common/actions';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
export class MessageService extends WorkbenchMessageService implements IChoiceService {
constructor(
container: HTMLElement,
@IWindowService private windowService: IWindowService,
@ITelemetryService telemetryService: ITelemetryService
) {
super(container, telemetryService);
}
public confirm(confirmation: IConfirmation): boolean {
const buttons: string[] = [];
if (confirmation.primaryButton) {
buttons.push(confirmation.primaryButton);
} else {
buttons.push(nls.localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"));
}
if (confirmation.secondaryButton) {
buttons.push(confirmation.secondaryButton);
} else if (typeof confirmation.secondaryButton === 'undefined') {
buttons.push(nls.localize('cancelButton', "Cancel"));
}
let opts: Electron.ShowMessageBoxOptions = {
title: confirmation.title,
message: confirmation.message,
buttons,
defaultId: 0,
cancelId: 1
};
if (confirmation.detail) {
opts.detail = confirmation.detail;
}
if (confirmation.type) {
opts.type = confirmation.type;
}
let result = this.showMessageBox(opts);
return result === 0 ? true : false;
}
public choose(severity: Severity, message: string, options: string[], cancelId: number, modal: boolean = false): TPromise<number> {
if (modal) {
const type: 'none' | 'info' | 'error' | 'question' | 'warning' = severity === Severity.Info ? 'question' : severity === Severity.Error ? 'error' : severity === Severity.Warning ? 'warning' : 'none';
return TPromise.wrap(this.showMessageBox({ message, buttons: options, type, cancelId }));
}
let onCancel: () => void = null;
const promise = new TPromise<number>((c, e) => {
const callback = (index: number) => () => {
c(index);
return TPromise.as(true);
};
const actions = options.map((option, index) => new Action('?', option, '', true, callback(index)));
onCancel = this.show(severity, { message, actions }, () => promise.cancel());
}, () => onCancel());
return promise;
}
private showMessageBox(opts: Electron.ShowMessageBoxOptions): number {
opts.buttons = opts.buttons.map(button => mnemonicButtonLabel(button));
opts.buttons = isLinux ? opts.buttons.reverse() : opts.buttons;
if (opts.defaultId !== void 0) {
opts.defaultId = isLinux ? opts.buttons.length - opts.defaultId - 1 : opts.defaultId;
}
if (opts.cancelId !== void 0) {
opts.cancelId = isLinux ? opts.buttons.length - opts.cancelId - 1 : opts.cancelId;
}
opts.noLink = true;
opts.title = opts.title || product.nameLong;
const result = this.windowService.showMessageBox(opts);
return isLinux ? opts.buttons.length - result - 1 : result;
}
}

View File

@@ -0,0 +1,207 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vs/nls';
import { onUnexpectedError } from 'vs/base/common/errors';
import * as paths from 'vs/base/common/paths';
import { TPromise } from 'vs/base/common/winjs.base';
import mime = require('vs/base/common/mime');
import { IFilesConfiguration } from 'vs/platform/files/common/files';
import { IExtensionService } from 'vs/platform/extensions/common/extensions';
import { IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint, ExtensionsRegistry } from 'vs/platform/extensions/common/extensionsRegistry';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { ILanguageExtensionPoint, IValidLanguageExtensionPoint } from 'vs/editor/common/services/modeService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl';
export const languagesExtPoint: IExtensionPoint<ILanguageExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<ILanguageExtensionPoint[]>('languages', [], {
description: nls.localize('vscode.extension.contributes.languages', 'Contributes language declarations.'),
type: 'array',
items: {
type: 'object',
defaultSnippets: [{ body: { id: '${1:languageId}', aliases: ['${2:label}'], extensions: ['${3:extension}'], configuration: './language-configuration.json' } }],
properties: {
id: {
description: nls.localize('vscode.extension.contributes.languages.id', 'ID of the language.'),
type: 'string'
},
aliases: {
description: nls.localize('vscode.extension.contributes.languages.aliases', 'Name aliases for the language.'),
type: 'array',
items: {
type: 'string'
}
},
extensions: {
description: nls.localize('vscode.extension.contributes.languages.extensions', 'File extensions associated to the language.'),
default: ['.foo'],
type: 'array',
items: {
type: 'string'
}
},
filenames: {
description: nls.localize('vscode.extension.contributes.languages.filenames', 'File names associated to the language.'),
type: 'array',
items: {
type: 'string'
}
},
filenamePatterns: {
description: nls.localize('vscode.extension.contributes.languages.filenamePatterns', 'File name glob patterns associated to the language.'),
type: 'array',
items: {
type: 'string'
}
},
mimetypes: {
description: nls.localize('vscode.extension.contributes.languages.mimetypes', 'Mime types associated to the language.'),
type: 'array',
items: {
type: 'string'
}
},
firstLine: {
description: nls.localize('vscode.extension.contributes.languages.firstLine', 'A regular expression matching the first line of a file of the language.'),
type: 'string'
},
configuration: {
description: nls.localize('vscode.extension.contributes.languages.configuration', 'A relative path to a file containing configuration options for the language.'),
type: 'string',
default: './language-configuration.json'
}
}
}
});
export class WorkbenchModeServiceImpl extends ModeServiceImpl {
private _configurationService: IConfigurationService;
private _extensionService: IExtensionService;
private _onReadyPromise: TPromise<boolean>;
constructor(
@IExtensionService extensionService: IExtensionService,
@IConfigurationService configurationService: IConfigurationService
) {
super();
this._configurationService = configurationService;
this._extensionService = extensionService;
languagesExtPoint.setHandler((extensions: IExtensionPointUser<ILanguageExtensionPoint[]>[]) => {
let allValidLanguages: IValidLanguageExtensionPoint[] = [];
for (let i = 0, len = extensions.length; i < len; i++) {
let extension = extensions[i];
if (!Array.isArray(extension.value)) {
extension.collector.error(nls.localize('invalid', "Invalid `contributes.{0}`. Expected an array.", languagesExtPoint.name));
continue;
}
for (let j = 0, lenJ = extension.value.length; j < lenJ; j++) {
let ext = extension.value[j];
if (isValidLanguageExtensionPoint(ext, extension.collector)) {
let configuration = (ext.configuration ? paths.join(extension.description.extensionFolderPath, ext.configuration) : ext.configuration);
allValidLanguages.push({
id: ext.id,
extensions: ext.extensions,
filenames: ext.filenames,
filenamePatterns: ext.filenamePatterns,
firstLine: ext.firstLine,
aliases: ext.aliases,
mimetypes: ext.mimetypes,
configuration: configuration
});
}
}
}
ModesRegistry.registerLanguages(allValidLanguages);
});
this._configurationService.onDidUpdateConfiguration(e => this.onConfigurationChange(this._configurationService.getConfiguration<IFilesConfiguration>()));
this.onDidCreateMode((mode) => {
this._extensionService.activateByEvent(`onLanguage:${mode.getId()}`).done(null, onUnexpectedError);
});
}
protected _onReady(): TPromise<boolean> {
if (!this._onReadyPromise) {
const configuration = this._configurationService.getConfiguration<IFilesConfiguration>();
this._onReadyPromise = this._extensionService.onReady().then(() => {
this.onConfigurationChange(configuration);
return true;
});
}
return this._onReadyPromise;
}
private onConfigurationChange(configuration: IFilesConfiguration): void {
// Clear user configured mime associations
mime.clearTextMimes(true /* user configured */);
// Register based on settings
if (configuration.files && configuration.files.associations) {
Object.keys(configuration.files.associations).forEach(pattern => {
const langId = configuration.files.associations[pattern];
const mimetype = this.getMimeForMode(langId) || `text/x-${langId}`;
mime.registerTextMime({ id: langId, mime: mimetype, filepattern: pattern, userConfigured: true });
});
}
}
}
function isUndefinedOrStringArray(value: string[]): boolean {
if (typeof value === 'undefined') {
return true;
}
if (!Array.isArray(value)) {
return false;
}
return value.every(item => typeof item === 'string');
}
function isValidLanguageExtensionPoint(value: ILanguageExtensionPoint, collector: ExtensionMessageCollector): boolean {
if (!value) {
collector.error(nls.localize('invalid.empty', "Empty value for `contributes.{0}`", languagesExtPoint.name));
return false;
}
if (typeof value.id !== 'string') {
collector.error(nls.localize('require.id', "property `{0}` is mandatory and must be of type `string`", 'id'));
return false;
}
if (!isUndefinedOrStringArray(value.extensions)) {
collector.error(nls.localize('opt.extensions', "property `{0}` can be omitted and must be of type `string[]`", 'extensions'));
return false;
}
if (!isUndefinedOrStringArray(value.filenames)) {
collector.error(nls.localize('opt.filenames', "property `{0}` can be omitted and must be of type `string[]`", 'filenames'));
return false;
}
if (typeof value.firstLine !== 'undefined' && typeof value.firstLine !== 'string') {
collector.error(nls.localize('opt.firstLine', "property `{0}` can be omitted and must be of type `string`", 'firstLine'));
return false;
}
if (typeof value.configuration !== 'undefined' && typeof value.configuration !== 'string') {
collector.error(nls.localize('opt.configuration', "property `{0}` can be omitted and must be of type `string`", 'configuration'));
return false;
}
if (!isUndefinedOrStringArray(value.aliases)) {
collector.error(nls.localize('opt.aliases', "property `{0}` can be omitted and must be of type `string[]`", 'aliases'));
return false;
}
if (!isUndefinedOrStringArray(value.mimetypes)) {
collector.error(nls.localize('opt.mimetypes', "property `{0}` can be omitted and must be of type `string[]`", 'mimetypes'));
return false;
}
return true;
}

View File

@@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Event from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
import { IPanel } from 'vs/workbench/common/panel';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
export const IPanelService = createDecorator<IPanelService>('panelService');
export interface IPanelIdentifier {
id: string;
name: string;
commandId: string;
}
export interface IPanelService {
_serviceBrand: ServiceIdentifier<any>;
onDidPanelOpen: Event<IPanel>;
onDidPanelClose: Event<IPanel>;
/**
* Opens a panel with the given identifier and pass keyboard focus to it if specified.
*/
openPanel(id: string, focus?: boolean): TPromise<IPanel>;
/**
* Returns the current active panel or null if none
*/
getActivePanel(): IPanel;
/**
* Returns all registered panels
*/
getPanels(): IPanelIdentifier[];
}

View File

@@ -0,0 +1,124 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import Event from 'vs/base/common/event';
export enum Parts {
ACTIVITYBAR_PART,
SIDEBAR_PART,
PANEL_PART,
EDITOR_PART,
STATUSBAR_PART,
TITLEBAR_PART
}
export enum Position {
LEFT,
RIGHT
}
export interface ILayoutOptions {
toggleMaximizedPanel?: boolean;
}
export const IPartService = createDecorator<IPartService>('partService');
export interface IPartService {
_serviceBrand: ServiceIdentifier<any>;
/**
* Emits when the visibility of the title bar changes.
*/
onTitleBarVisibilityChange: Event<void>;
/**
* Emits when the editor part's layout changes.
*/
onEditorLayout: Event<void>;
/**
* Asks the part service to layout all parts.
*/
layout(options?: ILayoutOptions): void;
/**
* Asks the part service to if all parts have been created.
*/
isCreated(): boolean;
/**
* Promise is complete when all parts have been created.
*/
joinCreation(): TPromise<boolean>;
/**
* Returns whether the given part has the keyboard focus or not.
*/
hasFocus(part: Parts): boolean;
/**
* Returns the parts HTML element, if there is one.
*/
getContainer(part: Parts): HTMLElement;
/**
* Returns if the part is visible.
*/
isVisible(part: Parts): boolean;
/**
* Set activity bar hidden or not
*/
setActivityBarHidden(hidden: boolean): void;
/**
* Number of pixels (adjusted for zooming) that the title bar (if visible) pushes down the workbench contents.
*/
getTitleBarOffset(): number;
/**
* Set sidebar hidden or not
*/
setSideBarHidden(hidden: boolean): TPromise<void>;
/**
* Set panel part hidden or not
*/
setPanelHidden(hidden: boolean): TPromise<void>;
/**
* Maximizes the panel height if the panel is not already maximized.
* Shrinks the panel to the default starting size if the panel is maximized.
*/
toggleMaximizedPanel(): void;
/**
* Returns true if the panel is maximized.
*/
isPanelMaximized(): boolean;
/**
* Gets the current side bar position. Note that the sidebar can be hidden too.
*/
getSideBarPosition(): Position;
/**
* Returns the identifier of the element that contains the workbench.
*/
getWorkbenchElementId(): string;
/**
* Toggles the workbench in and out of zen mode - parts get hidden and window goes fullscreen.
*/
toggleZenMode(): void;
/**
* Resizes currently focused part on main access
*/
resizePart(part: Parts, sizeChange: number): void;
}

View File

@@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g style="fill:white;">
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-workbench > .part.statusbar > .statusbar-item.progress {
background-image: url(progress.svg);
background-repeat: no-repeat;
background-position: left;
padding-left: 14px;
display: inline;
}
.monaco-workbench .progress-badge > .badge-content {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4=");
background-position: center center;
background-repeat: no-repeat;
}

View File

@@ -0,0 +1,257 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import lifecycle = require('vs/base/common/lifecycle');
import { TPromise } from 'vs/base/common/winjs.base';
import types = require('vs/base/common/types');
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IProgressService, IProgressRunner } from 'vs/platform/progress/common/progress';
interface ProgressState {
infinite?: boolean;
total?: number;
worked?: number;
done?: boolean;
whilePromise?: TPromise<any>;
}
export abstract class ScopedService {
protected toDispose: lifecycle.IDisposable[];
constructor(private viewletService: IViewletService, private panelService: IPanelService, private scopeId: string) {
this.toDispose = [];
this.registerListeners();
}
public registerListeners(): void {
this.toDispose.push(this.viewletService.onDidViewletOpen(viewlet => this.onScopeOpened(viewlet.getId())));
this.toDispose.push(this.panelService.onDidPanelOpen(panel => this.onScopeOpened(panel.getId())));
this.toDispose.push(this.viewletService.onDidViewletClose(viewlet => this.onScopeClosed(viewlet.getId())));
this.toDispose.push(this.panelService.onDidPanelClose(panel => this.onScopeClosed(panel.getId())));
}
private onScopeClosed(scopeId: string) {
if (scopeId === this.scopeId) {
this.onScopeDeactivated();
}
}
private onScopeOpened(scopeId: string) {
if (scopeId === this.scopeId) {
this.onScopeActivated();
}
}
public abstract onScopeActivated(): void;
public abstract onScopeDeactivated(): void;
}
export class WorkbenchProgressService extends ScopedService implements IProgressService {
public _serviceBrand: any;
private isActive: boolean;
private progressbar: ProgressBar;
private progressState: ProgressState;
constructor(
progressbar: ProgressBar,
scopeId: string,
isActive: boolean,
@IViewletService viewletService: IViewletService,
@IPanelService panelService: IPanelService
) {
super(viewletService, panelService, scopeId);
this.progressbar = progressbar;
this.isActive = isActive || types.isUndefinedOrNull(scopeId); // If service is unscoped, enable by default
this.progressState = Object.create(null);
}
public onScopeDeactivated(): void {
this.isActive = false;
}
public onScopeActivated(): void {
this.isActive = true;
// Return early if progress state indicates that progress is done
if (this.progressState.done) {
return;
}
// Replay Infinite Progress from Promise
if (this.progressState.whilePromise) {
this.doShowWhile();
}
// Replay Infinite Progress
else if (this.progressState.infinite) {
this.progressbar.infinite().getContainer().show();
}
// Replay Finite Progress (Total & Worked)
else {
if (this.progressState.total) {
this.progressbar.total(this.progressState.total).getContainer().show();
}
if (this.progressState.worked) {
this.progressbar.worked(this.progressState.worked).getContainer().show();
}
}
}
private clearProgressState(): void {
this.progressState.infinite = void 0;
this.progressState.done = void 0;
this.progressState.worked = void 0;
this.progressState.total = void 0;
this.progressState.whilePromise = void 0;
}
public show(infinite: boolean, delay?: number): IProgressRunner;
public show(total: number, delay?: number): IProgressRunner;
public show(infiniteOrTotal: any, delay?: number): IProgressRunner {
let infinite: boolean;
let total: number;
// Sort out Arguments
if (infiniteOrTotal === false || infiniteOrTotal === true) {
infinite = infiniteOrTotal;
} else {
total = infiniteOrTotal;
}
// Reset State
this.clearProgressState();
// Keep in State
this.progressState.infinite = infinite;
this.progressState.total = total;
// Active: Show Progress
if (this.isActive) {
// Infinite: Start Progressbar and Show after Delay
if (!types.isUndefinedOrNull(infinite)) {
if (types.isUndefinedOrNull(delay)) {
this.progressbar.infinite().getContainer().show();
} else {
this.progressbar.infinite().getContainer().showDelayed(delay);
}
}
// Finite: Start Progressbar and Show after Delay
else if (!types.isUndefinedOrNull(total)) {
if (types.isUndefinedOrNull(delay)) {
this.progressbar.total(total).getContainer().show();
} else {
this.progressbar.total(total).getContainer().showDelayed(delay);
}
}
}
return {
total: (total: number) => {
this.progressState.infinite = false;
this.progressState.total = total;
if (this.isActive) {
this.progressbar.total(total);
}
},
worked: (worked: number) => {
// Verify first that we are either not active or the progressbar has a total set
if (!this.isActive || this.progressbar.hasTotal()) {
this.progressState.infinite = false;
if (this.progressState.worked) {
this.progressState.worked += worked;
} else {
this.progressState.worked = worked;
}
if (this.isActive) {
this.progressbar.worked(worked);
}
}
// Otherwise the progress bar does not support worked(), we fallback to infinite() progress
else {
this.progressState.infinite = true;
this.progressState.worked = void 0;
this.progressState.total = void 0;
this.progressbar.infinite().getContainer().show();
}
},
done: () => {
this.progressState.infinite = false;
this.progressState.done = true;
if (this.isActive) {
this.progressbar.stop().getContainer().hide();
}
}
};
}
public showWhile(promise: TPromise<any>, delay?: number): TPromise<void> {
let stack: boolean = !!this.progressState.whilePromise;
// Reset State
if (!stack) {
this.clearProgressState();
}
// Otherwise join with existing running promise to ensure progress is accurate
else {
promise = TPromise.join([promise, this.progressState.whilePromise]);
}
// Keep Promise in State
this.progressState.whilePromise = promise;
let stop = () => {
// If this is not the last promise in the list of joined promises, return early
if (!!this.progressState.whilePromise && this.progressState.whilePromise !== promise) {
return;
}
// The while promise is either null or equal the promise we last hooked on
this.clearProgressState();
if (this.isActive) {
this.progressbar.stop().getContainer().hide();
}
};
this.doShowWhile(delay);
return promise.then(stop, stop);
}
private doShowWhile(delay?: number): void {
// Show Progress when active
if (this.isActive) {
if (types.isUndefinedOrNull(delay)) {
this.progressbar.infinite().getContainer().show();
} else {
this.progressbar.infinite().getContainer().showDelayed(delay);
}
}
}
public dispose(): void {
this.toDispose = lifecycle.dispose(this.toDispose);
}
}

View File

@@ -0,0 +1,188 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/progressService2';
import * as dom from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { IActivityBarService, ProgressBadge } from 'vs/workbench/services/activity/common/activityBarService';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IProgressService2, IProgressOptions, ProgressLocation, IProgress, IProgressStep, Progress, emptyProgress } from 'vs/platform/progress/common/progress';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { OcticonLabel } from 'vs/base/browser/ui/octiconLabel/octiconLabel';
import { Registry } from 'vs/platform/registry/common/platform';
import { StatusbarAlignment, IStatusbarRegistry, StatusbarItemDescriptor, Extensions, IStatusbarItem } from 'vs/workbench/browser/parts/statusbar/statusbar';
import { TPromise } from 'vs/base/common/winjs.base';
import { always } from 'vs/base/common/async';
class WindowProgressItem implements IStatusbarItem {
static Instance: WindowProgressItem;
private _element: HTMLElement;
private _label: OcticonLabel;
constructor() {
WindowProgressItem.Instance = this;
}
render(element: HTMLElement): IDisposable {
this._element = element;
this._label = new OcticonLabel(this._element);
this._element.classList.add('progress');
this.hide();
return null;
}
set text(value: string) {
this._label.text = value;
}
set title(value: string) {
this._label.title = value;
}
hide() {
dom.hide(this._element);
}
show() {
dom.show(this._element);
}
}
export class ProgressService2 implements IProgressService2 {
_serviceBrand: any;
private _stack: [IProgressOptions, Progress<IProgressStep>][] = [];
constructor(
@IActivityBarService private _activityBar: IActivityBarService,
@IViewletService private _viewletService: IViewletService
) {
//
}
withProgress(options: IProgressOptions, task: (progress: IProgress<{ message?: string, percentage?: number }>) => TPromise<any>): void {
const { location } = options;
switch (location) {
case ProgressLocation.Window:
this._withWindowProgress(options, task);
break;
case ProgressLocation.Scm:
this._withViewletProgress('workbench.view.scm', task);
break;
default:
console.warn(`Bad progress location: ${location}`);
}
}
private _withWindowProgress(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, percentage?: number }>) => TPromise<any>): void {
const task: [IProgressOptions, Progress<IProgressStep>] = [options, new Progress<IProgressStep>(() => this._updateWindowProgress())];
const promise = callback(task[1]);
let delayHandle = setTimeout(() => {
delayHandle = undefined;
this._stack.unshift(task);
this._updateWindowProgress();
// show progress for at least 150ms
always(TPromise.join([
TPromise.timeout(150),
promise
]), () => {
const idx = this._stack.indexOf(task);
this._stack.splice(idx, 1);
this._updateWindowProgress();
});
}, 150);
// cancel delay if promise finishes below 150ms
always(promise, () => clearTimeout(delayHandle));
}
private _updateWindowProgress(idx: number = 0) {
if (idx >= this._stack.length) {
WindowProgressItem.Instance.hide();
} else {
const [options, progress] = this._stack[idx];
let text = options.title;
if (progress.value && progress.value.message) {
text = progress.value.message;
}
if (!text) {
// no message -> no progress. try with next on stack
this._updateWindowProgress(idx + 1);
return;
}
let title = text;
if (options.title && options.title !== title) {
title = localize('progress.subtitle', "{0} - {1}", options.title, title);
}
if (options.tooltip) {
title = localize('progress.title', "{0}: {1}", options.tooltip, title);
}
WindowProgressItem.Instance.text = text;
WindowProgressItem.Instance.title = title;
WindowProgressItem.Instance.show();
}
}
private _withViewletProgress(viewletId: string, task: (progress: IProgress<{ message?: string, percentage?: number }>) => TPromise<any>): void {
const promise = task(emptyProgress);
// show in viewlet
const viewletProgress = this._viewletService.getProgressIndicator(viewletId);
if (viewletProgress) {
viewletProgress.showWhile(promise);
}
// show activity bar
let activityProgress: IDisposable;
let delayHandle = setTimeout(() => {
delayHandle = undefined;
const handle = this._activityBar.showActivity(
viewletId,
new ProgressBadge(() => ''),
'progress-badge'
);
const startTimeVisible = Date.now();
const minTimeVisible = 300;
activityProgress = {
dispose() {
const d = Date.now() - startTimeVisible;
if (d < minTimeVisible) {
// should at least show for Nms
setTimeout(() => handle.dispose(), minTimeVisible - d);
} else {
// shown long enough
handle.dispose();
}
}
};
}, 300);
always(promise, () => {
clearTimeout(delayHandle);
dispose(activityProgress);
});
}
}
Registry.as<IStatusbarRegistry>(Extensions.Statusbar).registerStatusbarItem(
new StatusbarItemDescriptor(WindowProgressItem, StatusbarAlignment.LEFT)
);

View File

@@ -0,0 +1,291 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as assert from 'assert';
import { IAction, IActionItem } from 'vs/base/common/actions';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import { IEditorControl } from 'vs/platform/editor/common/editor';
import { Viewlet, ViewletDescriptor } from 'vs/workbench/browser/viewlet';
import { IPanel } from 'vs/workbench/common/panel';
import { WorkbenchProgressService, ScopedService } from 'vs/workbench/services/progress/browser/progressService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { Emitter } from 'vs/base/common/event';
let activeViewlet: Viewlet = <any>{};
class TestViewletService implements IViewletService {
public _serviceBrand: any;
onDidViewletOpenEmitter = new Emitter<IViewlet>();
onDidViewletCloseEmitter = new Emitter<IViewlet>();
onDidViewletOpen = this.onDidViewletOpenEmitter.event;
onDidViewletClose = this.onDidViewletCloseEmitter.event;
public openViewlet(id: string, focus?: boolean): TPromise<IViewlet> {
return TPromise.as(null);
}
public getViewlets(): ViewletDescriptor[] {
return [];
}
public getActiveViewlet(): IViewlet {
return activeViewlet;
}
public dispose() {
}
public getDefaultViewletId(): string {
return 'workbench.view.explorer';
}
public getViewlet(id: string): ViewletDescriptor {
return null;
}
public getProgressIndicator(id: string) {
return null;
}
}
class TestPanelService implements IPanelService {
public _serviceBrand: any;
onDidPanelOpen = new Emitter<IPanel>().event;
onDidPanelClose = new Emitter<IPanel>().event;
public openPanel(id: string, focus?: boolean): Promise {
return TPromise.as(null);
}
public getPanels(): any[] {
return [];
}
public getActivePanel(): IViewlet {
return activeViewlet;
}
public dispose() {
}
}
class TestViewlet implements IViewlet {
constructor(private id: string) { }
getId(): string {
return this.id;
}
/**
* Returns the name of this composite to show in the title area.
*/
getTitle(): string {
return this.id;
}
/**
* Returns the primary actions of the composite.
*/
getActions(): IAction[] {
return [];
}
/**
* Returns the secondary actions of the composite.
*/
getSecondaryActions(): IAction[] {
return [];
}
/**
* Returns an array of actions to show in the context menu of the composite
*/
public getContextMenuActions(): IAction[] {
return [];
}
/**
* Returns the action item for a specific action.
*/
getActionItem(action: IAction): IActionItem {
return null;
}
/**
* Returns the underlying control of this composite.
*/
getControl(): IEditorControl {
return null;
}
/**
* Asks the underlying control to focus.
*/
focus(): void {
}
getOptimalWidth(): number {
return 10;
}
}
class TestScopedService extends ScopedService {
public isActive: boolean;
constructor(viewletService: IViewletService, panelService: IPanelService, scopeId: string) {
super(viewletService, panelService, scopeId);
}
public onScopeActivated() {
this.isActive = true;
}
public onScopeDeactivated() {
this.isActive = false;
}
}
class TestProgressBar {
public fTotal: number;
public fWorked: number;
public fInfinite: boolean;
public fDone: boolean;
constructor() {
}
public infinite() {
this.fDone = null;
this.fInfinite = true;
return this;
}
public total(total: number) {
this.fDone = null;
this.fTotal = total;
return this;
}
public hasTotal() {
return !!this.fTotal;
}
public worked(worked: number) {
this.fDone = null;
if (this.fWorked) {
this.fWorked += worked;
} else {
this.fWorked = worked;
}
return this;
}
public done() {
this.fDone = true;
this.fInfinite = null;
this.fWorked = null;
this.fTotal = null;
return this;
}
public stop() {
return this.done();
}
public getContainer() {
return {
show: function () { },
hide: function () { }
};
}
}
suite('Progress Service', () => {
test('ScopedService', () => {
let viewletService = new TestViewletService();
let panelService = new TestPanelService();
let service = new TestScopedService(viewletService, panelService, 'test.scopeId');
const testViewlet = new TestViewlet('test.scopeId');
assert(!service.isActive);
viewletService.onDidViewletOpenEmitter.fire(testViewlet);
assert(service.isActive);
viewletService.onDidViewletCloseEmitter.fire(testViewlet);
assert(!service.isActive);
});
test('WorkbenchProgressService', function () {
let testProgressBar = new TestProgressBar();
let viewletService = new TestViewletService();
let panelService = new TestPanelService();
let service = new WorkbenchProgressService((<any>testProgressBar), 'test.scopeId', true, viewletService, panelService);
// Active: Show (Infinite)
let fn = service.show(true);
assert.strictEqual(true, testProgressBar.fInfinite);
fn.done();
assert.strictEqual(true, testProgressBar.fDone);
// Active: Show (Total / Worked)
fn = service.show(100);
assert.strictEqual(false, !!testProgressBar.fInfinite);
assert.strictEqual(100, testProgressBar.fTotal);
fn.worked(20);
assert.strictEqual(20, testProgressBar.fWorked);
fn.total(80);
assert.strictEqual(80, testProgressBar.fTotal);
fn.done();
assert.strictEqual(true, testProgressBar.fDone);
// Inactive: Show (Infinite)
const testViewlet = new TestViewlet('test.scopeId');
viewletService.onDidViewletCloseEmitter.fire(testViewlet);
service.show(true);
assert.strictEqual(false, !!testProgressBar.fInfinite);
viewletService.onDidViewletOpenEmitter.fire(testViewlet);
assert.strictEqual(true, testProgressBar.fInfinite);
// Inactive: Show (Total / Worked)
viewletService.onDidViewletCloseEmitter.fire(testViewlet);
fn = service.show(100);
fn.total(80);
fn.worked(20);
assert.strictEqual(false, !!testProgressBar.fTotal);
viewletService.onDidViewletOpenEmitter.fire(testViewlet);
assert.strictEqual(20, testProgressBar.fWorked);
assert.strictEqual(80, testProgressBar.fTotal);
// Acive: Show While
let p = TPromise.as(null);
service.showWhile(p).then(() => {
assert.strictEqual(true, testProgressBar.fDone);
viewletService.onDidViewletCloseEmitter.fire(testViewlet);
p = TPromise.as(null);
service.showWhile(p).then(() => {
assert.strictEqual(true, testProgressBar.fDone);
viewletService.onDidViewletOpenEmitter.fire(testViewlet);
assert.strictEqual(true, testProgressBar.fDone);
});
});
});
});

View File

@@ -0,0 +1,80 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import Event from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Command } from 'vs/editor/common/modes';
export interface IBaselineResourceProvider {
getBaselineResource(resource: URI): TPromise<URI>;
}
export const ISCMService = createDecorator<ISCMService>('scm');
export interface ISCMResourceDecorations {
icon?: URI;
iconDark?: URI;
tooltip?: string;
strikeThrough?: boolean;
faded?: boolean;
}
export interface ISCMResource {
readonly resourceGroup: ISCMResourceGroup;
readonly sourceUri: URI;
readonly command?: Command;
readonly decorations: ISCMResourceDecorations;
}
export interface ISCMResourceGroup {
readonly provider: ISCMProvider;
readonly label: string;
readonly id: string;
readonly resources: ISCMResource[];
}
export interface ISCMProvider extends IDisposable {
readonly label: string;
readonly id: string;
readonly contextValue: string;
readonly resources: ISCMResourceGroup[];
readonly onDidChange: Event<void>;
readonly count?: number;
readonly commitTemplate?: string;
readonly onDidChangeCommitTemplate?: Event<string>;
readonly acceptInputCommand?: Command;
readonly statusBarCommands?: Command[];
getOriginalResource(uri: URI): TPromise<URI>;
}
export interface ISCMInput {
value: string;
readonly onDidChange: Event<string>;
}
export interface ISCMRepository extends IDisposable {
readonly onDidFocus: Event<void>;
readonly provider: ISCMProvider;
readonly input: ISCMInput;
focus(): void;
}
export interface ISCMService {
readonly _serviceBrand: any;
readonly onDidAddRepository: Event<ISCMRepository>;
readonly onDidRemoveRepository: Event<ISCMRepository>;
readonly onDidChangeRepository: Event<ISCMRepository>;
readonly repositories: ISCMRepository[];
registerSCMProvider(provider: ISCMProvider): ISCMRepository;
}

View File

@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository } from './scm';
class SCMInput implements ISCMInput {
private _value = '';
get value(): string {
return this._value;
}
set value(value: string) {
this._value = value;
this._onDidChange.fire(value);
}
private _onDidChange = new Emitter<string>();
get onDidChange(): Event<string> { return this._onDidChange.event; }
}
class SCMRepository implements ISCMRepository {
private _onDidFocus = new Emitter<void>();
readonly onDidFocus: Event<void> = this._onDidFocus.event;
readonly input: ISCMInput = new SCMInput();
constructor(
public readonly provider: ISCMProvider,
private disposable: IDisposable
) { }
focus(): void {
this._onDidFocus.fire();
}
dispose(): void {
this.disposable.dispose();
this.provider.dispose();
}
}
export class SCMService implements ISCMService {
_serviceBrand;
private _providerIds = new Set<string>();
private _repositories: ISCMRepository[] = [];
get repositories(): ISCMRepository[] { return [...this._repositories]; }
private _onDidAddProvider = new Emitter<ISCMRepository>();
get onDidAddRepository(): Event<ISCMRepository> { return this._onDidAddProvider.event; }
private _onDidRemoveProvider = new Emitter<ISCMRepository>();
get onDidRemoveRepository(): Event<ISCMRepository> { return this._onDidRemoveProvider.event; }
private _onDidChangeProvider = new Emitter<ISCMRepository>();
get onDidChangeRepository(): Event<ISCMRepository> { return this._onDidChangeProvider.event; }
constructor() { }
registerSCMProvider(provider: ISCMProvider): ISCMRepository {
if (this._providerIds.has(provider.id)) {
throw new Error(`SCM Provider ${provider.id} already exists.`);
}
this._providerIds.add(provider.id);
const disposable = toDisposable(() => {
const index = this._repositories.indexOf(repository);
if (index < 0) {
return;
}
this._providerIds.delete(provider.id);
this._repositories.splice(index, 1);
this._onDidRemoveProvider.fire(repository);
});
const repository = new SCMRepository(provider, disposable);
this._repositories.push(repository);
this._onDidAddProvider.fire(repository);
return repository;
}
}

View File

@@ -0,0 +1,741 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as childProcess from 'child_process';
import { StringDecoder, NodeStringDecoder } from 'string_decoder';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import fs = require('fs');
import path = require('path');
import { isEqualOrParent } from 'vs/base/common/paths';
import { Readable } from 'stream';
import { TPromise } from 'vs/base/common/winjs.base';
import scorer = require('vs/base/common/scorer');
import objects = require('vs/base/common/objects');
import arrays = require('vs/base/common/arrays');
import platform = require('vs/base/common/platform');
import strings = require('vs/base/common/strings');
import types = require('vs/base/common/types');
import glob = require('vs/base/common/glob');
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
import extfs = require('vs/base/node/extfs');
import flow = require('vs/base/node/flow');
import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search';
enum Traversal {
Node = 1,
MacFind,
WindowsDir,
LinuxFind
}
interface IDirectoryEntry {
base: string;
relativePath: string;
basename: string;
}
interface IDirectoryTree {
rootEntries: IDirectoryEntry[];
pathToEntries: { [relativePath: string]: IDirectoryEntry[] };
}
export class FileWalker {
private config: IRawSearch;
private filePattern: string;
private normalizedFilePatternLowercase: string;
private includePattern: glob.ParsedExpression;
private maxResults: number;
private maxFilesize: number;
private isLimitHit: boolean;
private resultCount: number;
private isCanceled: boolean;
private fileWalkStartTime: number;
private directoriesWalked: number;
private filesWalked: number;
private traversal: Traversal;
private errors: string[];
private cmdForkStartTime: number;
private cmdForkResultTime: number;
private cmdResultCount: number;
private folderExcludePatterns: Map<string, AbsoluteAndRelativeParsedExpression>;
private globalExcludePattern: glob.ParsedExpression;
private walkedPaths: { [path: string]: boolean; };
constructor(config: IRawSearch) {
this.config = config;
this.filePattern = config.filePattern;
this.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || null;
this.maxFilesize = config.maxFilesize || null;
this.walkedPaths = Object.create(null);
this.resultCount = 0;
this.isLimitHit = false;
this.directoriesWalked = 0;
this.filesWalked = 0;
this.traversal = Traversal.Node;
this.errors = [];
if (this.filePattern) {
this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
}
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
this.folderExcludePatterns = new Map<string, AbsoluteAndRelativeParsedExpression>();
config.folderQueries.forEach(folderQuery => {
const folderExcludeExpression: glob.IExpression = objects.assign({}, folderQuery.excludePattern || {}, this.config.excludePattern || {});
// Add excludes for other root folders
config.folderQueries
.map(rootFolderQuery => rootFolderQuery.folder)
.filter(rootFolder => rootFolder !== folderQuery.folder)
.forEach(otherRootFolder => {
// Exclude nested root folders
if (isEqualOrParent(otherRootFolder, folderQuery.folder)) {
folderExcludeExpression[path.relative(folderQuery.folder, otherRootFolder)] = true;
}
});
this.folderExcludePatterns.set(folderQuery.folder, new AbsoluteAndRelativeParsedExpression(folderExcludeExpression, folderQuery.folder));
});
}
public cancel(): void {
this.isCanceled = true;
}
public walk(folderQueries: IFolderSearch[], extraFiles: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error, isLimitHit: boolean) => void): void {
this.fileWalkStartTime = Date.now();
// Support that the file pattern is a full path to a file that exists
this.checkFilePatternAbsoluteMatch((exists, size) => {
if (this.isCanceled) {
return done(null, this.isLimitHit);
}
// Report result from file pattern if matching
if (exists) {
this.resultCount++;
onResult({
relativePath: this.filePattern,
basename: path.basename(this.filePattern),
size
});
// Optimization: a match on an absolute path is a good result and we do not
// continue walking the entire root paths array for other matches because
// it is very unlikely that another file would match on the full absolute path
return done(null, this.isLimitHit);
}
// For each extra file
if (extraFiles) {
extraFiles.forEach(extraFilePath => {
const basename = path.basename(extraFilePath);
if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) {
return; // excluded
}
// File: Check for match on file pattern and include pattern
this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename });
});
}
let traverse = this.nodeJSTraversal;
if (!this.maxFilesize) {
if (platform.isMacintosh) {
this.traversal = Traversal.MacFind;
traverse = this.findTraversal;
// Disable 'dir' for now (#11181, #11179, #11183, #11182).
} /* else if (platform.isWindows) {
this.traversal = Traversal.WindowsDir;
traverse = this.windowsDirTraversal;
} */ else if (platform.isLinux) {
this.traversal = Traversal.LinuxFind;
traverse = this.findTraversal;
}
}
const isNodeTraversal = traverse === this.nodeJSTraversal;
if (!isNodeTraversal) {
this.cmdForkStartTime = Date.now();
}
// For each root folder
flow.parallel<IFolderSearch, void>(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => {
this.call(traverse, this, folderQuery, onResult, (err?: Error) => {
if (err) {
if (isNodeTraversal) {
rootFolderDone(err, undefined);
} else {
// fallback
const errorMessage = toErrorMessage(err);
console.error(errorMessage);
this.errors.push(errorMessage);
this.nodeJSTraversal(folderQuery, onResult, err => rootFolderDone(err, undefined));
}
} else {
rootFolderDone(undefined, undefined);
}
});
}, (err, result) => {
done(err ? err[0] : null, this.isLimitHit);
});
});
}
private call(fun: Function, that: any, ...args: any[]): void {
try {
fun.apply(that, args);
} catch (e) {
args[args.length - 1](e);
}
}
private findTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, cb: (err?: Error) => void): void {
const rootFolder = folderQuery.folder;
const isMac = platform.isMacintosh;
let done = (err?: Error) => {
done = () => { };
cb(err);
};
let leftover = '';
let first = true;
const tree = this.initDirectoryTree();
const cmd = this.spawnFindCmd(folderQuery);
this.collectStdout(cmd, 'utf8', (err: Error, stdout?: string, last?: boolean) => {
if (err) {
done(err);
return;
}
// Mac: uses NFD unicode form on disk, but we want NFC
const normalized = leftover + (isMac ? strings.normalizeNFC(stdout) : stdout);
const relativeFiles = normalized.split('\n./');
if (first && normalized.length >= 2) {
first = false;
relativeFiles[0] = relativeFiles[0].trim().substr(2);
}
if (last) {
const n = relativeFiles.length;
relativeFiles[n - 1] = relativeFiles[n - 1].trim();
if (!relativeFiles[n - 1]) {
relativeFiles.pop();
}
} else {
leftover = relativeFiles.pop();
}
if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
done(new Error('Splitting up files failed'));
return;
}
this.addDirectoryEntries(tree, rootFolder, relativeFiles, onResult);
if (last) {
this.matchDirectoryTree(tree, rootFolder, onResult);
done();
}
});
}
// protected windowsDirTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
// const cmd = childProcess.spawn('cmd', ['/U', '/c', 'dir', '/s', '/b', '/a-d', rootFolder]);
// this.readStdout(cmd, 'ucs2', (err: Error, stdout?: string) => {
// if (err) {
// done(err);
// return;
// }
// const relativeFiles = stdout.split(`\r\n${rootFolder}\\`);
// relativeFiles[0] = relativeFiles[0].trim().substr(rootFolder.length + 1);
// const n = relativeFiles.length;
// relativeFiles[n - 1] = relativeFiles[n - 1].trim();
// if (!relativeFiles[n - 1]) {
// relativeFiles.pop();
// }
// if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
// done(new Error('Splitting up files failed'));
// return;
// }
// this.matchFiles(rootFolder, relativeFiles, onResult);
// done();
// });
// }
/**
* Public for testing.
*/
public spawnFindCmd(folderQuery: IFolderSearch) {
const excludePattern = this.folderExcludePatterns.get(folderQuery.folder);
const basenames = excludePattern.getBasenameTerms();
const pathTerms = excludePattern.getPathTerms();
let args = ['-L', '.'];
if (basenames.length || pathTerms.length) {
args.push('-not', '(', '(');
for (const basename of basenames) {
args.push('-name', basename);
args.push('-o');
}
for (const path of pathTerms) {
args.push('-path', path);
args.push('-o');
}
args.pop();
args.push(')', '-prune', ')');
}
args.push('-type', 'f');
return childProcess.spawn('find', args, { cwd: folderQuery.folder });
}
/**
* Public for testing.
*/
public readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string) => void): void {
let all = '';
this.collectStdout(cmd, encoding, (err: Error, stdout?: string, last?: boolean) => {
if (err) {
cb(err);
return;
}
all += stdout;
if (last) {
cb(null, all);
}
});
}
private collectStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
let done = (err: Error, stdout?: string, last?: boolean) => {
if (err || last) {
done = () => { };
this.cmdForkResultTime = Date.now();
}
cb(err, stdout, last);
};
this.forwardData(cmd.stdout, encoding, done);
const stderr = this.collectData(cmd.stderr);
cmd.on('error', (err: Error) => {
done(err);
});
cmd.on('close', (code: number) => {
if (code !== 0) {
done(new Error(`find failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
} else {
done(null, '', true);
}
});
}
private forwardData(stream: Readable, encoding: string, cb: (err: Error, stdout?: string) => void): NodeStringDecoder {
const decoder = new StringDecoder(encoding);
stream.on('data', (data: Buffer) => {
cb(null, decoder.write(data));
});
return decoder;
}
private collectData(stream: Readable): Buffer[] {
const buffers: Buffer[] = [];
stream.on('data', (data: Buffer) => {
buffers.push(data);
});
return buffers;
}
private decodeData(buffers: Buffer[], encoding: string): string {
const decoder = new StringDecoder(encoding);
return buffers.map(buffer => decoder.write(buffer)).join('');
}
private initDirectoryTree(): IDirectoryTree {
const tree: IDirectoryTree = {
rootEntries: [],
pathToEntries: Object.create(null)
};
tree.pathToEntries['.'] = tree.rootEntries;
return tree;
}
private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
this.cmdResultCount += relativeFiles.length;
// Support relative paths to files from a root resource (ignores excludes)
if (relativeFiles.indexOf(this.filePattern) !== -1) {
const basename = path.basename(this.filePattern);
this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
}
function add(relativePath: string) {
const basename = path.basename(relativePath);
const dirname = path.dirname(relativePath);
let entries = pathToEntries[dirname];
if (!entries) {
entries = pathToEntries[dirname] = [];
add(dirname);
}
entries.push({
base,
relativePath,
basename
});
}
relativeFiles.forEach(add);
}
private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
const self = this;
const excludePattern = this.folderExcludePatterns.get(rootFolder);
const filePattern = this.filePattern;
function matchDirectory(entries: IDirectoryEntry[]) {
self.directoriesWalked++;
for (let i = 0, n = entries.length; i < n; i++) {
const entry = entries[i];
const { relativePath, basename } = entry;
// Check exclude pattern
// If the user searches for the exact file name, we adjust the glob matching
// to ignore filtering by siblings because the user seems to know what she
// is searching for and we want to include the result in that case anyway
if (excludePattern.test(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) {
continue;
}
const sub = pathToEntries[relativePath];
if (sub) {
matchDirectory(sub);
} else {
self.filesWalked++;
if (relativePath === filePattern) {
continue; // ignore file if its path matches with the file pattern because that is already matched above
}
self.matchFile(onResult, entry);
}
};
}
matchDirectory(rootEntries);
}
private nodeJSTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
this.directoriesWalked++;
extfs.readdir(folderQuery.folder, (error: Error, files: string[]) => {
if (error || this.isCanceled || this.isLimitHit) {
return done();
}
// Support relative paths to files from a root resource (ignores excludes)
return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
if (this.isCanceled || this.isLimitHit) {
return done();
}
// Report result from file pattern if matching
if (match) {
this.resultCount++;
onResult({
base: folderQuery.folder,
relativePath: this.filePattern,
basename: path.basename(this.filePattern),
size
});
}
return this.doWalk(folderQuery, '', files, onResult, done);
});
});
}
public getStats(): IUncachedSearchStats {
return {
fromCache: false,
traversal: Traversal[this.traversal],
errors: this.errors,
fileWalkStartTime: this.fileWalkStartTime,
fileWalkResultTime: Date.now(),
directoriesWalked: this.directoriesWalked,
filesWalked: this.filesWalked,
resultCount: this.resultCount,
cmdForkStartTime: this.cmdForkStartTime,
cmdForkResultTime: this.cmdForkResultTime,
cmdResultCount: this.cmdResultCount
};
}
private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
if (!this.filePattern || !path.isAbsolute(this.filePattern)) {
return clb(false);
}
return fs.stat(this.filePattern, (error, stat) => {
return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
});
}
private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
if (!this.filePattern || path.isAbsolute(this.filePattern)) {
return clb(null);
}
const absolutePath = path.join(basePath, this.filePattern);
return fs.stat(absolutePath, (error, stat) => {
return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
});
}
private doWalk(folderQuery: IFolderSearch, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void {
const rootFolder = folderQuery.folder;
// Execute tasks on each file in parallel to optimize throughput
flow.parallel(files, (file: string, clb: (error: Error, result: {}) => void): void => {
// Check canceled
if (this.isCanceled || this.isLimitHit) {
return clb(null, undefined);
}
// If the user searches for the exact file name, we adjust the glob matching
// to ignore filtering by siblings because the user seems to know what she
// is searching for and we want to include the result in that case anyway
let siblings = files;
if (this.config.filePattern === file) {
siblings = [];
}
// Check exclude pattern
let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file;
if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, () => siblings)) {
return clb(null, undefined);
}
// Use lstat to detect links
let currentAbsolutePath = [rootFolder, currentRelativePath].join(path.sep);
fs.lstat(currentAbsolutePath, (error, lstat) => {
if (error || this.isCanceled || this.isLimitHit) {
return clb(null, undefined);
}
// If the path is a link, we must instead use fs.stat() to find out if the
// link is a directory or not because lstat will always return the stat of
// the link which is always a file.
this.statLinkIfNeeded(currentAbsolutePath, lstat, (error, stat) => {
if (error || this.isCanceled || this.isLimitHit) {
return clb(null, undefined);
}
// Directory: Follow directories
if (stat.isDirectory()) {
this.directoriesWalked++;
// to really prevent loops with links we need to resolve the real path of them
return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
if (error || this.isCanceled || this.isLimitHit) {
return clb(null, undefined);
}
if (this.walkedPaths[realpath]) {
return clb(null, undefined); // escape when there are cycles (can happen with symlinks)
}
this.walkedPaths[realpath] = true; // remember as walked
// Continue walking
return extfs.readdir(currentAbsolutePath, (error: Error, children: string[]): void => {
if (error || this.isCanceled || this.isLimitHit) {
return clb(null, undefined);
}
this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err, undefined));
});
});
}
// File: Check for match on file pattern and include pattern
else {
this.filesWalked++;
if (currentRelativePath === this.filePattern) {
return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
}
if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
return clb(null, undefined); // ignore file if max file size is hit
}
this.matchFile(onResult, { base: rootFolder, relativePath: currentRelativePath, basename: file, size: stat.size });
}
// Unwind
return clb(null, undefined);
});
});
}, (error: Error[]): void => {
if (error) {
error = arrays.coalesce(error); // find any error by removing null values first
}
return done(error && error.length > 0 ? error[0] : null);
});
}
private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
this.resultCount++;
if (this.maxResults && this.resultCount > this.maxResults) {
this.isLimitHit = true;
}
if (!this.isLimitHit) {
onResult(candidate);
}
}
}
private isFilePatternMatch(path: string): boolean {
// Check for search pattern
if (this.filePattern) {
if (this.filePattern === '*') {
return true; // support the all-matching wildcard
}
return scorer.matches(path, this.normalizedFilePatternLowercase);
}
// No patterns means we match all
return true;
}
private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
if (lstat.isSymbolicLink()) {
return fs.stat(path, clb); // stat the target the link points to
}
return clb(null, lstat); // not a link, so the stat is already ok for us
}
private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
if (lstat.isSymbolicLink()) {
return fs.realpath(path, (error, realpath) => {
if (error) {
return clb(error);
}
return clb(null, realpath);
});
}
return clb(null, path);
}
}
export class Engine implements ISearchEngine<IRawFileMatch> {
private folderQueries: IFolderSearch[];
private extraFiles: string[];
private walker: FileWalker;
constructor(config: IRawSearch) {
this.folderQueries = config.folderQueries;
this.extraFiles = config.extraFiles;
this.walker = new FileWalker(config);
}
public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
this.walker.walk(this.folderQueries, this.extraFiles, onResult, (err: Error, isLimitHit: boolean) => {
done(err, {
limitHit: isLimitHit,
stats: this.walker.getStats()
});
});
}
public cancel(): void {
this.walker.cancel();
}
}
/**
* This class exists to provide one interface on top of two ParsedExpressions, one for absolute expressions and one for relative expressions.
* The absolute and relative expressions don't "have" to be kept separate, but this keeps us from having to path.join every single
* file searched, it's only used for a text search with a searchPath
*/
class AbsoluteAndRelativeParsedExpression {
private absoluteParsedExpr: glob.ParsedExpression;
private relativeParsedExpr: glob.ParsedExpression;
constructor(expr: glob.IExpression, private root: string) {
this.init(expr);
}
/**
* Split the IExpression into its absolute and relative components, and glob.parse them separately.
*/
private init(expr: glob.IExpression): void {
let absoluteGlobExpr: glob.IExpression;
let relativeGlobExpr: glob.IExpression;
Object.keys(expr)
.filter(key => expr[key])
.forEach(key => {
if (path.isAbsolute(key)) {
absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
absoluteGlobExpr[key] = expr[key];
} else {
relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
relativeGlobExpr[key] = expr[key];
}
});
this.absoluteParsedExpr = absoluteGlobExpr && glob.parse(absoluteGlobExpr, { trimForExclusions: true });
this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, { trimForExclusions: true });
}
public test(_path: string, basename?: string, siblingsFn?: () => string[] | TPromise<string[]>): string | TPromise<string> {
return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, siblingsFn)) ||
(this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, siblingsFn));
}
public getBasenameTerms(): string[] {
const basenameTerms = [];
if (this.absoluteParsedExpr) {
basenameTerms.push(...glob.getBasenameTerms(this.absoluteParsedExpr));
}
if (this.relativeParsedExpr) {
basenameTerms.push(...glob.getBasenameTerms(this.relativeParsedExpr));
}
return basenameTerms;
}
public getPathTerms(): string[] {
const pathTerms = [];
if (this.absoluteParsedExpr) {
pathTerms.push(...glob.getPathTerms(this.absoluteParsedExpr));
}
if (this.relativeParsedExpr) {
pathTerms.push(...glob.getPathTerms(this.relativeParsedExpr));
}
return pathTerms;
}
}

View File

@@ -0,0 +1,496 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import fs = require('fs');
import { isAbsolute, sep } from 'path';
import gracefulFs = require('graceful-fs');
gracefulFs.gracefulify(fs);
import arrays = require('vs/base/common/arrays');
import { compareByScore } from 'vs/base/common/comparers';
import objects = require('vs/base/common/objects');
import scorer = require('vs/base/common/scorer');
import strings = require('vs/base/common/strings');
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import { FileWalker, Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch';
import { MAX_FILE_SIZE } from 'vs/platform/files/common/files';
import { RipgrepEngine } from 'vs/workbench/services/search/node/ripgrepTextSearch';
import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/textSearch';
import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider';
import { IRawSearchService, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine, IFileSearchProgressItem } from './search';
import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search';
export class SearchService implements IRawSearchService {
private static BATCH_SIZE = 512;
private caches: { [cacheKey: string]: Cache; } = Object.create(null);
private textSearchWorkerProvider: TextSearchWorkerProvider;
public fileSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return this.doFileSearch(FileSearchEngine, config, SearchService.BATCH_SIZE);
}
public textSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return config.useRipgrep ?
this.ripgrepTextSearch(config) :
this.legacyTextSearch(config);
}
public ripgrepTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
config.maxFilesize = MAX_FILE_SIZE;
let engine = new RipgrepEngine(config);
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
// Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned
const collector = new BatchedCollector<ISerializedFileMatch>(SearchService.BATCH_SIZE, p);
engine.search((match) => {
collector.addItem(match, match.numMatches);
}, (message) => {
p(message);
}, (error, stats) => {
collector.flush();
if (error) {
e(error);
} else {
c(stats);
}
});
}, () => {
engine.cancel();
});
}
public legacyTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
if (!this.textSearchWorkerProvider) {
this.textSearchWorkerProvider = new TextSearchWorkerProvider();
}
let engine = new TextSearchEngine(
config,
new FileWalker({
folderQueries: config.folderQueries,
extraFiles: config.extraFiles,
includePattern: config.includePattern,
excludePattern: config.excludePattern,
filePattern: config.filePattern,
maxFilesize: MAX_FILE_SIZE
}),
this.textSearchWorkerProvider);
return this.doTextSearch(engine, SearchService.BATCH_SIZE);
}
public doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine<IRawFileMatch>; }, config: IRawSearch, batchSize?: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
if (config.sortByScore) {
let sortedSearch = this.trySortedSearchFromCache(config);
if (!sortedSearch) {
const walkerConfig = config.maxResults ? objects.assign({}, config, { maxResults: null }) : config;
const engine = new EngineClass(walkerConfig);
sortedSearch = this.doSortedSearch(engine, config);
}
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
process.nextTick(() => { // allow caller to register progress callback first
sortedSearch.then(([result, rawMatches]) => {
const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch));
this.sendProgress(serializedMatches, p, batchSize);
c(result);
}, e, p);
});
}, () => {
sortedSearch.cancel();
});
}
let searchPromise: PPromise<void, IFileSearchProgressItem>;
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
const engine = new EngineClass(config);
searchPromise = this.doSearch(engine, batchSize)
.then(c, e, progress => {
if (Array.isArray(progress)) {
p(progress.map(m => this.rawMatchToSearchItem(m)));
} else if ((<IRawFileMatch>progress).relativePath) {
p(this.rawMatchToSearchItem(<IRawFileMatch>progress));
} else {
p(<IProgress>progress);
}
});
}, () => {
searchPromise.cancel();
});
}
private rawMatchToSearchItem(match: IRawFileMatch): ISerializedFileMatch {
return { path: match.base ? [match.base, match.relativePath].join(sep) : match.relativePath };
}
private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
let searchPromise: PPromise<void, IFileSearchProgressItem>;
let allResultsPromise = new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>((c, e, p) => {
let results: IRawFileMatch[] = [];
searchPromise = this.doSearch(engine, -1)
.then(result => {
c([result, results]);
}, e, progress => {
if (Array.isArray(progress)) {
results = progress;
} else {
p(progress);
}
});
}, () => {
searchPromise.cancel();
});
let cache: Cache;
if (config.cacheKey) {
cache = this.getOrCreateCache(config.cacheKey);
cache.resultsToSearchCache[config.filePattern] = allResultsPromise;
allResultsPromise.then(null, err => {
delete cache.resultsToSearchCache[config.filePattern];
});
allResultsPromise = this.preventCancellation(allResultsPromise);
}
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => {
allResultsPromise.then(([result, results]) => {
const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null);
const unsortedResultTime = Date.now();
const sortedResults = this.sortResults(config, results, scorerCache);
const sortedResultTime = Date.now();
c([{
stats: objects.assign({}, result.stats, {
unsortedResultTime,
sortedResultTime
}),
limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults
}, sortedResults]);
}, e, p);
}, () => {
allResultsPromise.cancel();
});
}
private getOrCreateCache(cacheKey: string): Cache {
const existing = this.caches[cacheKey];
if (existing) {
return existing;
}
return this.caches[cacheKey] = new Cache();
}
private trySortedSearchFromCache(config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
const cache = config.cacheKey && this.caches[config.cacheKey];
if (!cache) {
return undefined;
}
const cacheLookupStartTime = Date.now();
const cached = this.getResultsFromCache(cache, config.filePattern);
if (cached) {
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => {
cached.then(([result, results, cacheStats]) => {
const cacheLookupResultTime = Date.now();
const sortedResults = this.sortResults(config, results, cache.scorerCache);
const sortedResultTime = Date.now();
const stats: ICachedSearchStats = {
fromCache: true,
cacheLookupStartTime: cacheLookupStartTime,
cacheFilterStartTime: cacheStats.cacheFilterStartTime,
cacheLookupResultTime: cacheLookupResultTime,
cacheEntryCount: cacheStats.cacheFilterResultCount,
resultCount: results.length
};
if (config.sortByScore) {
stats.unsortedResultTime = cacheLookupResultTime;
stats.sortedResultTime = sortedResultTime;
}
if (!cacheStats.cacheWasResolved) {
stats.joined = result.stats;
}
c([
{
limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults,
stats: stats
},
sortedResults
]);
}, e, p);
}, () => {
cached.cancel();
});
}
return undefined;
}
private sortResults(config: IRawSearch, results: IRawFileMatch[], scorerCache: ScorerCache): IRawFileMatch[] {
const filePattern = config.filePattern;
const normalizedSearchValue = strings.stripWildcards(filePattern).toLowerCase();
const compare = (elementA: IRawFileMatch, elementB: IRawFileMatch) => compareByScore(elementA, elementB, FileMatchAccessor, filePattern, normalizedSearchValue, scorerCache);
return arrays.top(results, compare, config.maxResults);
}
private sendProgress(results: ISerializedFileMatch[], progressCb: (batch: ISerializedFileMatch[]) => void, batchSize: number) {
if (batchSize && batchSize > 0) {
for (let i = 0; i < results.length; i += batchSize) {
progressCb(results.slice(i, i + batchSize));
}
} else {
progressCb(results);
}
}
private getResultsFromCache(cache: Cache, searchValue: string): PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress> {
if (isAbsolute(searchValue)) {
return null; // bypass cache if user looks up an absolute path where matching goes directly on disk
}
// Find cache entries by prefix of search value
const hasPathSep = searchValue.indexOf(sep) >= 0;
let cached: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>;
let wasResolved: boolean;
for (let previousSearch in cache.resultsToSearchCache) {
// If we narrow down, we might be able to reuse the cached results
if (strings.startsWith(searchValue, previousSearch)) {
if (hasPathSep && previousSearch.indexOf(sep) < 0) {
continue; // since a path character widens the search for potential more matches, require it in previous search too
}
const c = cache.resultsToSearchCache[previousSearch];
c.then(() => { wasResolved = false; });
wasResolved = true;
cached = this.preventCancellation(c);
break;
}
}
if (!cached) {
return null;
}
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress>((c, e, p) => {
cached.then(([complete, cachedEntries]) => {
const cacheFilterStartTime = Date.now();
// Pattern match on results
let results: IRawFileMatch[] = [];
const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase();
for (let i = 0; i < cachedEntries.length; i++) {
let entry = cachedEntries[i];
// Check if this entry is a match for the search value
if (!scorer.matches(entry.relativePath, normalizedSearchValueLowercase)) {
continue;
}
results.push(entry);
}
c([complete, results, {
cacheWasResolved: wasResolved,
cacheFilterStartTime: cacheFilterStartTime,
cacheFilterResultCount: cachedEntries.length
}]);
}, e, p);
}, () => {
cached.cancel();
});
}
private doTextSearch(engine: TextSearchEngine, batchSize: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
// Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned
const collector = new BatchedCollector<ISerializedFileMatch>(batchSize, p);
engine.search((matches) => {
const totalMatches = matches.reduce((acc, m) => acc + m.numMatches, 0);
collector.addItems(matches, totalMatches);
}, (progress) => {
p(progress);
}, (error, stats) => {
collector.flush();
if (error) {
e(error);
} else {
c(stats);
}
});
}, () => {
engine.cancel();
});
}
private doSearch(engine: ISearchEngine<IRawFileMatch>, batchSize?: number): PPromise<ISerializedSearchComplete, IFileSearchProgressItem> {
return new PPromise<ISerializedSearchComplete, IFileSearchProgressItem>((c, e, p) => {
let batch: IRawFileMatch[] = [];
engine.search((match) => {
if (match) {
if (batchSize) {
batch.push(match);
if (batchSize > 0 && batch.length >= batchSize) {
p(batch);
batch = [];
}
} else {
p(match);
}
}
}, (progress) => {
p(progress);
}, (error, stats) => {
if (batch.length) {
p(batch);
}
if (error) {
e(error);
} else {
c(stats);
}
});
}, () => {
engine.cancel();
});
}
public clearCache(cacheKey: string): TPromise<void> {
delete this.caches[cacheKey];
return TPromise.as(undefined);
}
private preventCancellation<C, P>(promise: PPromise<C, P>): PPromise<C, P> {
return new PPromise<C, P>((c, e, p) => {
// Allow for piled up cancellations to come through first.
process.nextTick(() => {
promise.then(c, e, p);
});
}, () => {
// Do not propagate.
});
}
}
class Cache {
public resultsToSearchCache: { [searchValue: string]: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>; } = Object.create(null);
public scorerCache: ScorerCache = Object.create(null);
}
interface ScorerCache {
[key: string]: number;
}
class FileMatchAccessor {
public static getLabel(match: IRawFileMatch): string {
return match.basename;
}
public static getResourcePath(match: IRawFileMatch): string {
return match.relativePath;
}
}
interface CacheStats {
cacheWasResolved: boolean;
cacheFilterStartTime: number;
cacheFilterResultCount: number;
}
/**
* Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
* set of items collected.
* But after that point, the callback is called with batches of maxBatchSize.
* If the batch isn't filled within some time, the callback is also called.
*/
class BatchedCollector<T> {
private static TIMEOUT = 4000;
// After RUN_TIMEOUT_UNTIL_COUNT items have been collected, stop flushing on timeout
private static START_BATCH_AFTER_COUNT = 50;
private totalNumberCompleted = 0;
private batch: T[] = [];
private batchSize = 0;
private timeoutHandle: number;
constructor(private maxBatchSize: number, private cb: (items: T | T[]) => void) {
}
addItem(item: T, size: number): void {
if (!item) {
return;
}
if (this.maxBatchSize > 0) {
this.addItemToBatch(item, size);
} else {
this.cb(item);
}
}
addItems(items: T[], size: number): void {
if (!items) {
return;
}
if (this.maxBatchSize > 0) {
this.addItemsToBatch(items, size);
} else {
this.cb(items);
}
}
private addItemToBatch(item: T, size: number): void {
this.batch.push(item);
this.batchSize += size;
this.onUpdate();
}
private addItemsToBatch(item: T[], size: number): void {
this.batch = this.batch.concat(item);
this.batchSize += size;
this.onUpdate();
}
private onUpdate(): void {
if (this.totalNumberCompleted < BatchedCollector.START_BATCH_AFTER_COUNT) {
// Flush because we aren't batching yet
this.flush();
} else if (this.batchSize >= this.maxBatchSize) {
// Flush because the batch is full
this.flush();
} else if (!this.timeoutHandle) {
// No timeout running, start a timeout to flush
this.timeoutHandle = setTimeout(() => {
this.flush();
}, BatchedCollector.TIMEOUT);
}
}
flush(): void {
if (this.batchSize) {
this.totalNumberCompleted += this.batchSize;
this.cb(this.batch);
this.batch = [];
this.batchSize = 0;
if (this.timeoutHandle) {
clearTimeout(this.timeoutHandle);
this.timeoutHandle = 0;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More