mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 02:48:30 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
@@ -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;
|
||||
}
|
||||
80
src/vs/workbench/services/backup/common/backup.ts
Normal file
80
src/vs/workbench/services/backup/common/backup.ts
Normal 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>;
|
||||
}
|
||||
268
src/vs/workbench/services/backup/node/backupFileService.ts
Normal file
268
src/vs/workbench/services/backup/node/backupFileService.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
@@ -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`
|
||||
};
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
916
src/vs/workbench/services/configuration/node/configuration.ts
Normal file
916
src/vs/workbench/services/configuration/node/configuration.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
363
src/vs/workbench/services/editor/browser/editorService.ts
Normal file
363
src/vs/workbench/services/editor/browser/editorService.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
94
src/vs/workbench/services/editor/common/editorService.ts
Normal file
94
src/vs/workbench/services/editor/common/editorService.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
39
src/vs/workbench/services/extensions/node/barrier.ts
Normal file
39
src/vs/workbench/services/extensions/node/barrier.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
110
src/vs/workbench/services/extensions/node/lazyPromise.ts
Normal file
110
src/vs/workbench/services/extensions/node/lazyPromise.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
198
src/vs/workbench/services/extensions/node/rpcProtocol.ts
Normal file
198
src/vs/workbench/services/extensions/node/rpcProtocol.ts
Normal 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))}}`;
|
||||
}
|
||||
}
|
||||
308
src/vs/workbench/services/files/electron-browser/fileService.ts
Normal file
308
src/vs/workbench/services/files/electron-browser/fileService.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
981
src/vs/workbench/services/files/node/fileService.ts
Normal file
981
src/vs/workbench/services/files/node/fileService.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
119
src/vs/workbench/services/files/node/watcher/common.ts
Normal file
119
src/vs/workbench/services/files/node/watcher/common.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
23
src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts
Normal file
23
src/vs/workbench/services/files/node/watcher/nsfw/watcher.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
18
src/vs/workbench/services/files/node/watcher/unix/watcher.ts
Normal file
18
src/vs/workbench/services/files/node/watcher/unix/watcher.ts
Normal 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>;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
814
src/vs/workbench/services/files/test/node/fileService.test.ts
Normal file
814
src/vs/workbench/services/files/test/node/fileService.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 = {}));
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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 = {}));
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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 |
@@ -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 = {}));
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
Small File
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This is some UTF 8 with BOM file.
|
||||
188
src/vs/workbench/services/files/test/node/resolver.test.ts
Normal file
188
src/vs/workbench/services/files/test/node/resolver.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
18
src/vs/workbench/services/files/test/node/utils.ts
Normal file
18
src/vs/workbench/services/files/test/node/utils.ts
Normal 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;
|
||||
}
|
||||
226
src/vs/workbench/services/files/test/node/watcher.test.ts
Normal file
226
src/vs/workbench/services/files/test/node/watcher.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
134
src/vs/workbench/services/group/common/groupService.ts
Normal file
134
src/vs/workbench/services/group/common/groupService.ts
Normal 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;
|
||||
}
|
||||
708
src/vs/workbench/services/history/browser/history.ts
Normal file
708
src/vs/workbench/services/history/browser/history.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
63
src/vs/workbench/services/history/common/history.ts
Normal file
63
src/vs/workbench/services/history/common/history.ts
Normal 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;
|
||||
}
|
||||
265
src/vs/workbench/services/keybinding/common/keybindingEditing.ts
Normal file
265
src/vs/workbench/services/keybinding/common/keybindingEditing.ts
Normal 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 + '[]';
|
||||
}
|
||||
}
|
||||
194
src/vs/workbench/services/keybinding/common/keybindingIO.ts
Normal file
194
src/vs/workbench/services/keybinding/common/keybindingIO.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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
691
src/vs/workbench/services/keybinding/common/scanCode.ts
Normal file
691
src/vs/workbench/services/keybinding/common/scanCode.ts
Normal 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);
|
||||
})();
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
117
src/vs/workbench/services/message/browser/media/messageList.css
Normal file
117
src/vs/workbench/services/message/browser/media/messageList.css
Normal 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 */
|
||||
}
|
||||
490
src/vs/workbench/services/message/browser/messageList.ts
Normal file
490
src/vs/workbench/services/message/browser/messageList.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
150
src/vs/workbench/services/message/browser/messageService.ts
Normal file
150
src/vs/workbench/services/message/browser/messageService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
207
src/vs/workbench/services/mode/common/workbenchModeService.ts
Normal file
207
src/vs/workbench/services/mode/common/workbenchModeService.ts
Normal 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;
|
||||
}
|
||||
40
src/vs/workbench/services/panel/common/panelService.ts
Normal file
40
src/vs/workbench/services/panel/common/panelService.ts
Normal 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[];
|
||||
}
|
||||
124
src/vs/workbench/services/part/common/partService.ts
Normal file
124
src/vs/workbench/services/part/common/partService.ts
Normal 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;
|
||||
}
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
257
src/vs/workbench/services/progress/browser/progressService.ts
Normal file
257
src/vs/workbench/services/progress/browser/progressService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
188
src/vs/workbench/services/progress/browser/progressService2.ts
Normal file
188
src/vs/workbench/services/progress/browser/progressService2.ts
Normal 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)
|
||||
);
|
||||
291
src/vs/workbench/services/progress/test/progressService.test.ts
Normal file
291
src/vs/workbench/services/progress/test/progressService.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
80
src/vs/workbench/services/scm/common/scm.ts
Normal file
80
src/vs/workbench/services/scm/common/scm.ts
Normal 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;
|
||||
}
|
||||
95
src/vs/workbench/services/scm/common/scmService.ts
Normal file
95
src/vs/workbench/services/scm/common/scmService.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
741
src/vs/workbench/services/search/node/fileSearch.ts
Normal file
741
src/vs/workbench/services/search/node/fileSearch.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
496
src/vs/workbench/services/search/node/rawSearchService.ts
Normal file
496
src/vs/workbench/services/search/node/rawSearchService.ts
Normal 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
Reference in New Issue
Block a user