Merge VS Code 1.23.1 (#1520)

This commit is contained in:
Matt Irvine
2018-06-05 11:24:51 -07:00
committed by GitHub
parent e3baf5c443
commit 0c58f09e59
3651 changed files with 74249 additions and 48599 deletions

View File

@@ -13,7 +13,7 @@ import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKe
import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { TPromise } from 'vs/base/common/winjs.base';
import { ExtensionPointContribution, IExtensionDescription, IExtensionsStatus, IExtensionService, ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
import Event, { Emitter } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
// --- service instances

View File

@@ -7,17 +7,17 @@
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 * as crypto from 'crypto';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as pfs from '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 { FileService } from 'vs/workbench/services/files/electron-browser/fileService';
import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices';
import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { DefaultEndOfLine } from 'vs/editor/common/model';
@@ -39,7 +39,7 @@ const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', crypto.cre
class TestBackupFileService extends BackupFileService {
constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) {
const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true });
const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true });
super(workspaceBackupPath, fileService);
}
@@ -52,21 +52,19 @@ class TestBackupFileService extends BackupFileService {
suite('BackupFileService', () => {
let service: TestBackupFileService;
setup(done => {
setup(() => {
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();
});
return pfs.del(backupHome, os.tmpdir()).then(() => {
return pfs.mkdirp(backupHome).then(() => {
return pfs.writeFile(workspacesJsonPath, '');
});
});
});
teardown(done => {
extfs.del(backupHome, os.tmpdir(), done);
teardown(() => {
return pfs.del(backupHome, os.tmpdir());
});
suite('getBackupResource', () => {
@@ -90,16 +88,15 @@ suite('BackupFileService', () => {
});
suite('loadBackupResource', () => {
test('should return whether a backup resource exists', done => {
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
test('should return whether a backup resource exists', () => {
return pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
service.loadBackupResource(fooFile).then(resource => {
return 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();
});
});
});
@@ -107,165 +104,151 @@ suite('BackupFileService', () => {
});
suite('backupResource', () => {
test('text file', function (done: () => void) {
service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
test('text file', function () {
return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).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, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
test('untitled file', function () {
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).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();
});
});
test('text file (ITextSnapshot)', function (done: () => void) {
test('text file (ITextSnapshot)', function () {
const model = TextModel.createFromString('test');
service.backupResource(fooFile, model.createSnapshot()).then(() => {
return service.backupResource(fooFile, model.createSnapshot()).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`);
model.dispose();
done();
});
});
test('untitled file (ITextSnapshot)', function (done: () => void) {
test('untitled file (ITextSnapshot)', function () {
const model = TextModel.createFromString('test');
service.backupResource(untitledFile, model.createSnapshot()).then(() => {
return service.backupResource(untitledFile, model.createSnapshot()).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`);
model.dispose();
done();
});
});
test('text file (large file, ITextSnapshot)', function (done: () => void) {
test('text file (large file, ITextSnapshot)', function () {
const largeString = (new Array(10 * 1024)).join('Large String\n');
const model = TextModel.createFromString(largeString);
service.backupResource(fooFile, model.createSnapshot()).then(() => {
return service.backupResource(fooFile, model.createSnapshot()).then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
assert.equal(fs.existsSync(fooBackupPath), true);
assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\n${largeString}`);
model.dispose();
done();
});
});
test('untitled file (large file, ITextSnapshot)', function (done: () => void) {
test('untitled file (large file, ITextSnapshot)', function () {
const largeString = (new Array(10 * 1024)).join('Large String\n');
const model = TextModel.createFromString(largeString);
service.backupResource(untitledFile, model.createSnapshot()).then(() => {
return service.backupResource(untitledFile, model.createSnapshot()).then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
assert.equal(fs.existsSync(untitledBackupPath), true);
assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\n${largeString}`);
model.dispose();
done();
});
});
});
suite('discardResourceBackup', () => {
test('text file', function (done: () => void) {
service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
test('text file', function () {
return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
service.discardResourceBackup(fooFile).then(() => {
return 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, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
test('untitled file', function () {
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
service.discardResourceBackup(untitledFile).then(() => {
return service.discardResourceBackup(untitledFile).then(() => {
assert.equal(fs.existsSync(untitledBackupPath), false);
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 0);
done();
});
});
});
});
suite('discardAllWorkspaceBackups', () => {
test('text file', function (done: () => void) {
service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
test('text file', function () {
return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
return service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
service.discardAllWorkspaceBackups().then(() => {
return 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, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
test('untitled file', function () {
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
service.discardAllWorkspaceBackups().then(() => {
return 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, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
test('should disable further backups', function () {
return service.discardAllWorkspaceBackups().then(() => {
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
assert.equal(fs.existsSync(workspaceBackupPath), false);
done();
});
});
});
});
suite('getWorkspaceFileBackups', () => {
test('("file") - text file', done => {
service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
service.getWorkspaceFileBackups().then(textFiles => {
test('("file") - text file', () => {
return service.backupResource(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
return service.getWorkspaceFileBackups().then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath]);
service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
service.getWorkspaceFileBackups().then(textFiles => {
return service.backupResource(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
return service.getWorkspaceFileBackups().then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath, barFile.fsPath]);
done();
});
});
});
});
});
test('("file") - untitled file', done => {
service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
service.getWorkspaceFileBackups().then(textFiles => {
test('("file") - untitled file', () => {
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
return service.getWorkspaceFileBackups().then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), [untitledFile.fsPath]);
done();
});
});
});
test('("untitled") - untitled file', done => {
service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
service.getWorkspaceFileBackups().then(textFiles => {
test('("untitled") - untitled file', () => {
return service.backupResource(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false)).then(() => {
return service.getWorkspaceFileBackups().then(textFiles => {
assert.deepEqual(textFiles.map(f => f.fsPath), ['Untitled-1']);
done();
});
});
});
@@ -346,16 +329,14 @@ suite('BackupFilesModel', () => {
assert.equal(model.has(resource4), true);
});
test('resolve', (done) => {
pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
test('resolve', () => {
return pfs.mkdirp(path.dirname(fooBackupPath)).then(() => {
fs.writeFileSync(fooBackupPath, 'foo');
const model = new BackupFilesModel();
model.resolve(workspaceBackupPath).then(model => {
return model.resolve(workspaceBackupPath).then(model => {
assert.equal(model.has(Uri.file(fooBackupPath)), true);
done();
});
});
});

View File

@@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICommandService, ICommandEvent, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import Event, { Emitter } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { ILogService } from 'vs/platform/log/common/log';
@@ -18,7 +18,7 @@ export class CommandService extends Disposable implements ICommandService {
private _extensionHostIsReady: boolean = false;
private _onWillExecuteCommand: Emitter<ICommandEvent> = this._register(new Emitter<ICommandEvent>());
private readonly _onWillExecuteCommand: Emitter<ICommandEvent> = this._register(new Emitter<ICommandEvent>());
public readonly onWillExecuteCommand: Event<ICommandEvent> = this._onWillExecuteCommand.event;
constructor(

View File

@@ -12,7 +12,7 @@ import { CommandService } from 'vs/workbench/services/commands/common/commandSer
import { IExtensionService, ExtensionPointContribution, IExtensionDescription, ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import Event, { Emitter } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
import { NullLogService } from 'vs/platform/log/common/log';
class SimpleExtensionService implements IExtensionService {

View File

@@ -14,10 +14,6 @@ export const FOLDER_SETTINGS_PATH = `${FOLDER_CONFIG_FOLDER_NAME}/${FOLDER_SETTI
export const IWorkspaceConfigurationService = createDecorator<IWorkspaceConfigurationService>('configurationService');
export interface IWorkspaceConfigurationService extends IConfigurationService {
/**
* Returns untrusted configuration keys for the current workspace.
*/
getUnsupportedWorkspaceKeys(): string[];
}
export const defaultSettingsSchemaId = 'vscode://schemas/settings/default';
@@ -30,5 +26,5 @@ export const TASKS_CONFIGURATION_KEY = 'tasks';
export const LAUNCH_CONFIGURATION_KEY = 'launch';
export const WORKSPACE_STANDALONE_CONFIGURATIONS = Object.create(null);
WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/tasks.json`;
WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/launch.json`;
WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${TASKS_CONFIGURATION_KEY}.json`;
WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${LAUNCH_CONFIGURATION_KEY}.json`;

View File

@@ -32,13 +32,15 @@ const configurationEntrySchema: IJSONSchema = {
type: 'object',
properties: {
isExecutable: {
type: 'boolean'
type: 'boolean',
deprecationMessage: 'This property is deprecated. Instead use `scope` property and set it to `application` value.'
},
scope: {
type: 'string',
enum: ['window', 'resource'],
enum: ['application', 'window', 'resource'],
default: 'window',
enumDescriptions: [
nls.localize('scope.application.description', "Application specific configuration, which can be configured only in User settings."),
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.")
],
@@ -130,7 +132,17 @@ function validateProperties(configuration: IConfigurationNode, extension: IExten
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 (propertyConfiguration.scope) {
if (propertyConfiguration.scope.toString() === 'application') {
propertyConfiguration.scope = ConfigurationScope.APPLICATION;
} else if (propertyConfiguration.scope.toString() === 'resource') {
propertyConfiguration.scope = ConfigurationScope.RESOURCE;
} else {
propertyConfiguration.scope = ConfigurationScope.WINDOW;
}
} else {
propertyConfiguration.scope = ConfigurationScope.WINDOW;
}
propertyConfiguration.notMultiRootAdopted = !(extension.description.isBuiltin || (Array.isArray(extension.description.keywords) && extension.description.keywords.indexOf('multi-root ready') !== -1));
if (message) {
extension.collector.warn(message);

View File

@@ -5,30 +5,15 @@
'use strict';
import { equals } from 'vs/base/common/objects';
import { compare, toValuesTree, IConfigurationChangeEvent, ConfigurationTarget, IConfigurationModel, IConfigurationOverrides, IOverrides } from 'vs/platform/configuration/common/configuration';
import { compare, toValuesTree, IConfigurationChangeEvent, ConfigurationTarget, IConfigurationModel, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
import { Configuration as BaseConfiguration, ConfigurationModelParser, ConfigurationChangeEvent, ConfigurationModel, AbstractConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationRegistry, IConfigurationPropertySchema, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import { Workspace } from 'vs/platform/workspace/common/workspace';
import { StrictResourceMap } from 'vs/base/common/map';
import { ResourceMap } from 'vs/base/common/map';
import URI from 'vs/base/common/uri';
export class SettingsModel extends ConfigurationModel {
private _unsupportedKeys: string[];
constructor(contents: any, keys: string[], overrides: IOverrides[], unsupportedKeys: string[]) {
super(contents, keys, overrides);
this._unsupportedKeys = unsupportedKeys;
}
public get unsupportedKeys(): string[] {
return this._unsupportedKeys;
}
}
export class WorkspaceConfigurationModelParser extends ConfigurationModelParser {
private _folders: IStoredWorkspaceFolder[] = [];
@@ -37,7 +22,7 @@ export class WorkspaceConfigurationModelParser extends ConfigurationModelParser
constructor(name: string) {
super(name);
this._settingsModelParser = new FolderSettingsModelParser(name);
this._settingsModelParser = new FolderSettingsModelParser(name, [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]);
this._launchModel = new ConfigurationModel();
}
@@ -45,8 +30,8 @@ export class WorkspaceConfigurationModelParser extends ConfigurationModelParser
return this._folders;
}
get settingsModel(): SettingsModel {
return this._settingsModelParser.settingsModel;
get settingsModel(): ConfigurationModel {
return this._settingsModelParser.configurationModel;
}
get launchModel(): ConfigurationModel {
@@ -96,9 +81,9 @@ export class StandaloneConfigurationModelParser extends ConfigurationModelParser
export class FolderSettingsModelParser extends ConfigurationModelParser {
private _raw: any;
private _settingsModel: SettingsModel;
private _settingsModel: ConfigurationModel;
constructor(name: string, private configurationScope?: ConfigurationScope) {
constructor(name: string, private scopes: ConfigurationScope[]) {
super(name);
}
@@ -108,11 +93,7 @@ export class FolderSettingsModelParser extends ConfigurationModelParser {
}
get configurationModel(): ConfigurationModel {
return this._settingsModel || new SettingsModel({}, [], [], []);
}
get settingsModel(): SettingsModel {
return <SettingsModel>this.configurationModel;
return this._settingsModel || new ConfigurationModel();
}
reprocess(): void {
@@ -120,34 +101,22 @@ export class FolderSettingsModelParser extends ConfigurationModelParser {
}
private parseWorkspaceSettings(rawSettings: any): void {
const unsupportedKeys = [];
const rawWorkspaceSettings = {};
const configurationProperties = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurationProperties();
for (let key in rawSettings) {
if (this.isNotExecutable(key, configurationProperties)) {
if (this.configurationScope === void 0 || this.getScope(key, configurationProperties) === this.configurationScope) {
rawWorkspaceSettings[key] = rawSettings[key];
}
} else {
unsupportedKeys.push(key);
const scope = this.getScope(key, configurationProperties);
if (this.scopes.indexOf(scope) !== -1) {
rawWorkspaceSettings[key] = rawSettings[key];
}
}
const configurationModel = this.parseRaw(rawWorkspaceSettings);
this._settingsModel = new SettingsModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides, unsupportedKeys);
this._settingsModel = new ConfigurationModel(configurationModel.contents, configurationModel.keys, configurationModel.overrides);
}
private getScope(key: string, configurationProperties: { [qualifiedKey: string]: IConfigurationPropertySchema }): ConfigurationScope {
const propertySchema = configurationProperties[key];
return propertySchema ? propertySchema.scope : ConfigurationScope.WINDOW;
}
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;
}
}
export class Configuration extends BaseConfiguration {
@@ -156,9 +125,9 @@ export class Configuration extends BaseConfiguration {
defaults: ConfigurationModel,
user: ConfigurationModel,
workspaceConfiguration: ConfigurationModel,
folders: StrictResourceMap<ConfigurationModel>,
folders: ResourceMap<ConfigurationModel>,
memoryConfiguration: ConfigurationModel,
memoryConfigurationByResource: StrictResourceMap<ConfigurationModel>,
memoryConfigurationByResource: ResourceMap<ConfigurationModel>,
private readonly _workspace: Workspace) {
super(defaults, user, workspaceConfiguration, folders, memoryConfiguration, memoryConfigurationByResource);
}
@@ -266,8 +235,8 @@ export class AllKeysConfigurationChangeEvent extends AbstractConfigurationChange
return this._changedConfiguration;
}
get changedConfigurationByResource(): StrictResourceMap<IConfigurationModel> {
return new StrictResourceMap();
get changedConfigurationByResource(): ResourceMap<IConfigurationModel> {
return new ResourceMap();
}
get affectedKeys(): string[] {
@@ -287,7 +256,7 @@ export class WorkspaceConfigurationChangeEvent implements IConfigurationChangeEv
return this.configurationChangeEvent.changedConfiguration;
}
get changedConfigurationByResource(): StrictResourceMap<IConfigurationModel> {
get changedConfigurationByResource(): ResourceMap<IConfigurationModel> {
return this.configurationChangeEvent.changedConfigurationByResource;
}
@@ -317,4 +286,4 @@ export class WorkspaceConfigurationChangeEvent implements IConfigurationChangeEv
return false;
}
}
}

View File

@@ -4,74 +4,31 @@
*--------------------------------------------------------------------------------------------*/
import URI from 'vs/base/common/uri';
import { createHash } from 'crypto';
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 { readFile } from 'vs/base/node/pfs';
import { Event, Emitter } from 'vs/base/common/event';
import * as pfs from 'vs/base/node/pfs';
import * as errors from 'vs/base/common/errors';
import * as collections from 'vs/base/common/collections';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { RunOnceScheduler } from 'vs/base/common/async';
import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files';
import { FileChangeType, FileChangesEvent, IContent, IFileService } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
import { ConfigWatcher } from 'vs/base/node/config';
import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels';
import { ConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
import { WorkspaceConfigurationModelParser, FolderSettingsModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels';
import { WORKSPACE_STANDALONE_CONFIGURATIONS, FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration';
import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration';
import { IStoredWorkspace, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces';
import * as extfs from 'vs/base/node/extfs';
import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService';
import { WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { relative } from 'path';
import { equals } from 'vs/base/common/objects';
// node.hs helper functions
interface IStat {
resource: URI;
isDirectory?: boolean;
children?: { resource: URI; }[];
}
interface IContent {
resource: URI;
value: string;
}
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)) }; })
});
}
});
});
}
import { Schemas } from 'vs/base/common/network';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationModel } from 'vs/platform/configuration/common/configuration';
export class WorkspaceConfiguration extends Disposable {
@@ -79,7 +36,7 @@ export class WorkspaceConfiguration extends Disposable {
private _workspaceConfigurationWatcher: ConfigWatcher<WorkspaceConfigurationModelParser>;
private _workspaceConfigurationWatcherDisposables: IDisposable[] = [];
private _onDidUpdateConfiguration: Emitter<void> = this._register(new Emitter<void>());
private readonly _onDidUpdateConfiguration: Emitter<void> = this._register(new Emitter<void>());
public readonly onDidUpdateConfiguration: Event<void> = this._onDidUpdateConfiguration.event;
private _workspaceConfigurationModelParser: WorkspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(this._workspaceConfigPath ? this._workspaceConfigPath.fsPath : '');
@@ -135,10 +92,6 @@ export class WorkspaceConfiguration extends Disposable {
return this._cache;
}
getUnsupportedKeys(): string[] {
return this._workspaceConfigurationModelParser.settingsModel.unsupportedKeys;
}
reprocessWorkspaceSettings(): ConfigurationModel {
this._workspaceConfigurationModelParser.reprocessWorkspaceSettings();
this.consolidate();
@@ -170,108 +123,201 @@ export class WorkspaceConfiguration extends Disposable {
}
}
export class FolderConfiguration extends Disposable {
function isFolderConfigurationFile(resource: URI): boolean {
const name = paths.basename(resource.path);
return [`${FOLDER_SETTINGS_NAME}.json`, `${TASKS_CONFIGURATION_KEY}.json`, `${LAUNCH_CONFIGURATION_KEY}.json`].some(p => p === name);// only workspace config files
}
private static readonly RELOAD_CONFIGURATION_DELAY = 50;
export interface IFolderConfiguration {
readonly onDidChange: Event<void>;
readonly loaded: boolean;
loadConfiguration(): TPromise<ConfigurationModel>;
reprocess(): ConfigurationModel;
dispose(): void;
}
private bulkFetchFromWorkspacePromise: TPromise;
private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<ConfigurationModelParser> };
export abstract class AbstractFolderConfiguration extends Disposable implements IFolderConfiguration {
private _folderSettingsModelParser: FolderSettingsModelParser;
private _standAloneConfigurations: ConfigurationModel[] = [];
private _cache: ConfigurationModel = new ConfigurationModel();
private _standAloneConfigurations: ConfigurationModel[];
private _cache: ConfigurationModel;
private _loaded: boolean = false;
private reloadConfigurationScheduler: RunOnceScheduler;
private reloadConfigurationEventEmitter: Emitter<ConfigurationModel> = new Emitter<ConfigurationModel>();
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(private folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState) {
constructor(protected readonly folder: URI, workbenchState: WorkbenchState, from?: AbstractFolderConfiguration) {
super();
this._folderSettingsModelParser = new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? ConfigurationScope.RESOURCE : void 0);
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));
this._folderSettingsModelParser = from ? from._folderSettingsModelParser : new FolderSettingsModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? [ConfigurationScope.RESOURCE] : [ConfigurationScope.WINDOW, ConfigurationScope.RESOURCE]);
this._standAloneConfigurations = from ? from._standAloneConfigurations : [];
this._cache = from ? from._cache : new ConfigurationModel();
}
get loaded(): boolean {
return this._loaded;
}
loadConfiguration(): TPromise<ConfigurationModel> {
// Load workspace locals
return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => {
this._standAloneConfigurations = Object.keys(workspaceConfigFiles).filter(key => key !== FOLDER_SETTINGS_PATH).map(key => <ConfigurationModel>workspaceConfigFiles[key].configurationModel);
// Consolidate (support *.json files in the workspace settings folder)
this.consolidate();
return this._cache;
});
return this.loadFolderConfigurationContents()
.then((contents) => {
// reset
this._standAloneConfigurations = [];
this._folderSettingsModelParser.parse('');
// parse
this.parseContents(contents);
// Consolidate (support *.json files in the workspace settings folder)
this.consolidate();
this._loaded = true;
return this._cache;
});
}
reprocess(): ConfigurationModel {
const oldContents = this._folderSettingsModelParser.settingsModel.contents;
const oldContents = this._folderSettingsModelParser.configurationModel.contents;
this._folderSettingsModelParser.reprocess();
if (!equals(oldContents, this._folderSettingsModelParser.settingsModel.contents)) {
if (!equals(oldContents, this._folderSettingsModelParser.configurationModel.contents)) {
this.consolidate();
}
return this._cache;
}
getUnsupportedKeys(): string[] {
return this._folderSettingsModelParser.settingsModel.unsupportedKeys;
}
private consolidate(): void {
this._cache = this._folderSettingsModelParser.settingsModel.merge(...this._standAloneConfigurations);
this._cache = this._folderSettingsModelParser.configurationModel.merge(...this._standAloneConfigurations);
}
private loadWorkspaceConfigFiles(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModelParser }> {
// 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([]);
private parseContents(contents: { resource: URI, value: string }[]): void {
for (const content of contents) {
const name = paths.basename(content.resource.path);
if (name === `${FOLDER_SETTINGS_NAME}.json`) {
this._folderSettingsModelParser.parse(content.value);
} else {
const matches = /([^\.]*)*\.json/.exec(name);
if (matches && matches[1]) {
const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(content.resource.toString(), matches[1]);
standAloneConfigurationModelParser.parse(content.value);
this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel);
}
}
}
}
return resolveContents(stat.children.filter(stat => {
const isJson = paths.extname(stat.resource.fsPath) === '.json';
if (!isJson) {
return false; // only JSON files
protected abstract loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]>;
}
export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration {
private readonly folderConfigurationPath: URI;
constructor(folder: URI, configFolderRelativePath: string, workbenchState: WorkbenchState) {
super(folder, workbenchState);
this.folderConfigurationPath = URI.file(paths.join(this.folder.fsPath, configFolderRelativePath));
}
protected loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]> {
return this.resolveStat(this.folderConfigurationPath).then(stat => {
if (!stat.isDirectory) {
return TPromise.as([]);
}
return this.resolveContents(stat.children.filter(stat => isFolderConfigurationFile(stat.resource))
.map(stat => stat.resource));
}, err => [] /* never fail this call */)
.then(null, errors.onUnexpectedError);
}
private resolveContents(resources: URI[]): TPromise<{ resource: URI, value: string }[]> {
return TPromise.join(resources.map(resource =>
pfs.readFile(resource.fsPath)
.then(contents => ({ resource, value: contents.toString() }))));
}
private resolveStat(resource: URI): TPromise<{ resource: URI, isDirectory?: boolean, children?: { resource: URI; }[] }> {
return new TPromise<{ resource: URI, isDirectory?: boolean, children?: { resource: URI; }[] }>((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)) }; })
});
}
});
});
}
}
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.createConfigurationModelParser(content)));
}, errors.onUnexpectedError);
export class FileServiceBasedFolderConfiguration extends AbstractFolderConfiguration {
private bulkContentFetchromise: TPromise<any>;
private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise<IContent> };
private reloadConfigurationScheduler: RunOnceScheduler;
private readonly folderConfigurationPath: URI;
constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) {
super(folder, workbenchState, from);
this.folderConfigurationPath = folder.with({ path: paths.join(this.folder.path, configFolderRelativePath) });
this.workspaceFilePathToConfiguration = Object.create(null);
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e)));
}
protected loadFolderConfigurationContents(): TPromise<{ resource: URI, value: string }[]> {
// once: when invoked for the first time we fetch json files that contribute settings
if (!this.bulkContentFetchromise) {
this.bulkContentFetchromise = this.fileService.resolveFile(this.folderConfigurationPath)
.then(stat => {
if (stat.isDirectory && stat.children) {
stat.children
.filter(child => isFolderConfigurationFile(child.resource))
.forEach(child => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(child.resource)] = this.fileService.resolveContent(child.resource).then(null, errors.onUnexpectedError));
}
}).then(null, err => [] /* never fail this call */);
}
// 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));
return this.bulkContentFetchromise.then(() => TPromise.join(this.workspaceFilePathToConfiguration).then(result => collections.values(result)));
}
public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise<ConfigurationModel> {
private handleWorkspaceFileEvents(event: FileChangesEvent): void {
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));
const basename = paths.basename(resource.path);
const isJson = paths.extname(basename) === '.json';
const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && basename === 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
const folderRelativePath = this.toFolderRelativePath(resource);
if (!folderRelativePath) {
continue; // event is not inside folder
}
// Handle case where ".vscode" got deleted
if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) {
if (isDeletedSettingsFolder) {
this.workspaceFilePathToConfiguration = Object.create(null);
affectedByChanges = true;
}
// only valid workspace config files
if (!this.isWorkspaceConfigurationFile(workspacePath)) {
if (!isFolderConfigurationFile(resource)) {
continue;
}
@@ -279,72 +325,176 @@ export class FolderConfiguration extends Disposable {
// remove promises for delete events
switch (events[i].type) {
case FileChangeType.DELETED:
affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, workspacePath);
affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, folderRelativePath);
break;
case FileChangeType.UPDATED:
case FileChangeType.ADDED:
this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => this.createConfigurationModelParser(content), errors.onUnexpectedError);
this.workspaceFilePathToConfiguration[folderRelativePath] = this.fileService.resolveContent(resource).then(null, errors.onUnexpectedError);
affectedByChanges = true;
}
}
if (!affectedByChanges) {
return TPromise.as(null);
if (affectedByChanges) {
this.reloadConfigurationScheduler.schedule();
}
}
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 toFolderRelativePath(resource: URI): string {
if (resource.scheme === Schemas.file) {
if (paths.isEqualOrParent(resource.fsPath, this.folderConfigurationPath.fsPath, !isLinux /* ignorecase */)) {
return paths.normalize(relative(this.folderConfigurationPath.fsPath, resource.fsPath));
}
} else {
if (paths.isEqualOrParent(resource.path, this.folderConfigurationPath.path, true /* ignorecase */)) {
return paths.normalize(relative(this.folderConfigurationPath.path, resource.path));
}
}
return null;
}
}
export class CachedFolderConfiguration extends Disposable implements IFolderConfiguration {
private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
private readonly cachedFolderPath: string;
private readonly cachedConfigurationPath: string;
private configurationModel: ConfigurationModel;
loaded: boolean = false;
constructor(
folder: URI,
configFolderRelativePath: string,
environmentService: IEnvironmentService) {
super();
this.cachedFolderPath = paths.join(environmentService.appSettingsHome, createHash('md5').update(paths.join(folder.path, configFolderRelativePath)).digest('hex'));
this.cachedConfigurationPath = paths.join(this.cachedFolderPath, 'configuration.json');
this.configurationModel = new ConfigurationModel();
}
loadConfiguration(): TPromise<ConfigurationModel> {
return pfs.readFile(this.cachedConfigurationPath)
.then(contents => {
const parsed: IConfigurationModel = JSON.parse(contents.toString());
this.configurationModel = new ConfigurationModel(parsed.contents, parsed.keys, parsed.overrides);
this.loaded = true;
return this.configurationModel;
}, () => this.configurationModel);
}
updateConfiguration(configurationModel: ConfigurationModel): TPromise<void> {
const raw = JSON.stringify(configurationModel.toJSON());
return this.createCachedFolder().then(created => {
if (created) {
return configurationModel.keys.length ? pfs.writeFile(this.cachedConfigurationPath, raw) : pfs.rimraf(this.cachedFolderPath);
}
return null;
});
}
private createConfigurationModelParser(content: IContent): ConfigurationModelParser {
const path = this.toFolderRelativePath(content.resource);
if (path === FOLDER_SETTINGS_PATH) {
this._folderSettingsModelParser.parse(content.value);
return this._folderSettingsModelParser;
} else {
const matches = /\/([^\.]*)*\.json/.exec(path);
if (matches && matches[1]) {
const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(content.resource.toString(), matches[1]);
standAloneConfigurationModelParser.parse(content.value);
return standAloneConfigurationModelParser;
reprocess(): ConfigurationModel {
return this.configurationModel;
}
getUnsupportedKeys(): string[] {
return [];
}
private createCachedFolder(): TPromise<boolean> {
return pfs.exists(this.cachedFolderPath)
.then(null, () => false)
.then(exists => exists ? exists : pfs.mkdirp(this.cachedFolderPath).then(() => true, () => false));
}
}
export class FolderConfiguration extends Disposable implements IFolderConfiguration {
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
private folderConfiguration: IFolderConfiguration;
private cachedFolderConfiguration: CachedFolderConfiguration;
private _loaded: boolean = false;
constructor(
readonly workspaceFolder: IWorkspaceFolder,
private readonly configFolderRelativePath: string,
private readonly workbenchState: WorkbenchState,
private environmentService: IEnvironmentService,
fileService?: IFileService
) {
super();
this.cachedFolderConfiguration = new CachedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.environmentService);
this.folderConfiguration = this.cachedFolderConfiguration;
if (fileService) {
this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService);
} else if (this.workspaceFolder.uri.scheme === Schemas.file) {
this.folderConfiguration = new NodeBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState);
}
this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
}
loadConfiguration(): TPromise<ConfigurationModel> {
return this.folderConfiguration.loadConfiguration()
.then(model => {
this._loaded = this.folderConfiguration.loaded;
return model;
});
}
reprocess(): ConfigurationModel {
return this.folderConfiguration.reprocess();
}
get loaded(): boolean {
return this._loaded;
}
adopt(fileService: IFileService): TPromise<boolean> {
if (fileService) {
if (this.folderConfiguration instanceof CachedFolderConfiguration) {
return this.adoptFromCachedConfiguration(fileService);
}
if (this.folderConfiguration instanceof NodeBasedFolderConfiguration) {
return this.adoptFromNodeBasedConfiguration(fileService);
}
}
return new ConfigurationModelParser(null);
return TPromise.as(false);
}
private isWorkspaceConfigurationFile(folderRelativePath: string): boolean {
return [FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY], WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY]].some(p => p === folderRelativePath);
private adoptFromCachedConfiguration(fileService: IFileService): TPromise<boolean> {
const folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService);
return folderConfiguration.loadConfiguration()
.then(() => {
this.folderConfiguration = folderConfiguration;
this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
this.updateCache();
return true;
});
}
private toResource(folderRelativePath: string): URI {
if (typeof folderRelativePath === 'string') {
return URI.file(paths.join(this.folder.fsPath, folderRelativePath));
private adoptFromNodeBasedConfiguration(fileService: IFileService): TPromise<boolean> {
const oldFolderConfiguration = this.folderConfiguration;
this.folderConfiguration = new FileServiceBasedFolderConfiguration(this.workspaceFolder.uri, this.configFolderRelativePath, this.workbenchState, fileService, <AbstractFolderConfiguration>oldFolderConfiguration);
oldFolderConfiguration.dispose();
this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
return TPromise.as(false);
}
private onDidFolderConfigurationChange(): void {
this.updateCache();
this._onDidChange.fire();
}
private updateCache(): TPromise<void> {
if (this.workspaceFolder.uri.scheme !== Schemas.file && this.folderConfiguration instanceof FileServiceBasedFolderConfiguration) {
return this.folderConfiguration.loadConfiguration()
.then(configurationModel => this.cachedFolderConfiguration.updateConfiguration(configurationModel));
}
return null;
}
private toFolderRelativePath(resource: URI, toOSPath?: boolean): string {
if (this.contains(resource)) {
return paths.normalize(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;
return TPromise.as(null);
}
}

View File

@@ -30,7 +30,6 @@ import { OVERRIDE_PROPERTY_PATTERN, IConfigurationRegistry, Extensions as Config
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextModel } from 'vs/editor/common/model';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
export enum ConfigurationEditingErrorCode {
@@ -40,6 +39,11 @@ export enum ConfigurationEditingErrorCode {
*/
ERROR_UNKNOWN_KEY,
/**
* Error when trying to write an application setting into workspace settings.
*/
ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION,
/**
* Error when trying to write an invalid folder configuration key to folder settings.
*/
@@ -127,7 +131,6 @@ export class ConfigurationEditingService {
@IFileService private fileService: IFileService,
@ITextModelService private textModelResolverService: ITextModelService,
@ITextFileService private textFileService: ITextFileService,
@IChoiceService private choiceService: IChoiceService,
@INotificationService private notificationService: INotificationService,
@ICommandService private commandService: ICommandService,
@IWorkbenchEditorService private editorService: IWorkbenchEditorService
@@ -139,12 +142,12 @@ export class ConfigurationEditingService {
const operation = this.getConfigurationEditOperation(target, value, options.scopes || {});
return this.queue.queue(() => this.doWriteConfiguration(operation, options) // queue up writes to prevent race conditions
.then(() => null,
error => {
if (!options.donotNotifyError) {
this.onError(error, operation, options.scopes);
}
return TPromise.wrapError(error);
}));
error => {
if (!options.donotNotifyError) {
this.onError(error, operation, options.scopes);
}
return TPromise.wrapError(error);
}));
}
private doWriteConfiguration(operation: IConfigurationEditOperation, options: ConfigurationEditingOptions): TPromise<void> {
@@ -194,19 +197,19 @@ export class ConfigurationEditingService {
: operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration")
: null;
if (openStandAloneConfigurationActionLabel) {
this.choiceService.choose(Severity.Error, error.message, [openStandAloneConfigurationActionLabel])
.then(option => {
if (option === 0) {
this.openFile(operation.resource);
}
});
this.notificationService.prompt(Severity.Error, error.message,
[{
label: openStandAloneConfigurationActionLabel,
run: () => this.openFile(operation.resource)
}]
);
} else {
this.choiceService.choose(Severity.Error, error.message, [nls.localize('open', "Open Settings")])
.then(option => {
if (option === 0) {
this.openSettings(operation);
}
});
this.notificationService.prompt(Severity.Error, error.message,
[{
label: nls.localize('open', "Open Settings"),
run: () => this.openSettings(operation)
}]
);
}
}
@@ -215,30 +218,30 @@ export class ConfigurationEditingService {
: operation.workspaceStandAloneConfigurationKey === LAUNCH_CONFIGURATION_KEY ? nls.localize('openLaunchConfiguration', "Open Launch Configuration")
: null;
if (openStandAloneConfigurationActionLabel) {
this.choiceService.choose(Severity.Error, error.message, [nls.localize('saveAndRetry', "Save and Retry"), openStandAloneConfigurationActionLabel])
.then(option => {
switch (option) {
case 0 /* Save & Retry */:
const key = operation.key ? `${operation.workspaceStandAloneConfigurationKey}.${operation.key}` : operation.workspaceStandAloneConfigurationKey;
this.writeConfiguration(operation.target, { key, value: operation.value }, <ConfigurationEditingOptions>{ force: true, scopes });
break;
case 1 /* Open Config */:
this.openFile(operation.resource);
break;
this.notificationService.prompt(Severity.Error, error.message,
[{
label: nls.localize('saveAndRetry', "Save and Retry"),
run: () => {
const key = operation.key ? `${operation.workspaceStandAloneConfigurationKey}.${operation.key}` : operation.workspaceStandAloneConfigurationKey;
this.writeConfiguration(operation.target, { key, value: operation.value }, <ConfigurationEditingOptions>{ force: true, scopes });
}
});
},
{
label: openStandAloneConfigurationActionLabel,
run: () => this.openFile(operation.resource)
}]
);
} else {
this.choiceService.choose(Severity.Error, error.message, [nls.localize('saveAndRetry', "Save and Retry"), nls.localize('open', "Open Settings")])
.then(option => {
switch (option) {
case 0 /* Save and Retry */:
this.writeConfiguration(operation.target, { key: operation.key, value: operation.value }, <ConfigurationEditingOptions>{ force: true, scopes });
break;
case 1 /* Open Settings */:
this.openSettings(operation);
break;
}
});
this.notificationService.prompt(Severity.Error, error.message,
[{
label: nls.localize('saveAndRetry', "Save and Retry"),
run: () => this.writeConfiguration(operation.target, { key: operation.key, value: operation.value }, <ConfigurationEditingOptions>{ force: true, scopes })
},
{
label: nls.localize('open', "Open Settings"),
run: () => this.openSettings(operation)
}]
);
}
}
@@ -276,6 +279,7 @@ export class ConfigurationEditingService {
// 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_WORKSPACE_CONFIGURATION_APPLICATION: return nls.localize('errorInvalidWorkspaceConfigurationApplication', "Unable to write {0} to Workspace Settings. This setting can be written only into User settings.", 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_WORKSPACE_TARGET: return nls.localize('errorInvalidWorkspaceTarget', "Unable to write to Workspace Settings because {0} does not support for workspace scope in a multi folder workspace.", operation.key);
@@ -398,6 +402,15 @@ export class ConfigurationEditingService {
return this.wrapError(ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED, target, operation);
}
if (target === ConfigurationTarget.WORKSPACE) {
if (!operation.workspaceStandAloneConfigurationKey) {
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
if (configurationProperties[operation.key].scope === ConfigurationScope.APPLICATION) {
return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, target, operation);
}
}
}
if (target === ConfigurationTarget.WORKSPACE_FOLDER) {
if (!operation.resource) {
return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation);

View File

@@ -8,15 +8,15 @@ import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { dirname, basename } from 'path';
import * as assert from 'vs/base/common/assert';
import Event, { Emitter } from 'vs/base/common/event';
import { StrictResourceMap } from 'vs/base/common/map';
import { equals } from 'vs/base/common/objects';
import { Event, Emitter } from 'vs/base/common/event';
import { ResourceMap } from 'vs/base/common/map';
import { equals, deepClone } from 'vs/base/common/objects';
import { Disposable } from 'vs/base/common/lifecycle';
import { Queue } from 'vs/base/common/async';
import { stat, writeFile } from 'vs/base/node/pfs';
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { FileChangesEvent } from 'vs/platform/files/common/files';
import { IFileService } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ConfigurationChangeEvent, ConfigurationModel, DefaultConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
@@ -24,7 +24,7 @@ import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides
import { Configuration, WorkspaceConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels';
import { IWorkspaceConfigurationService, FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration';
import { Registry } from 'vs/platform/registry/common/platform';
import { IConfigurationNode, IConfigurationRegistry, Extensions, settingsSchema, resourceSettingsSchema, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { IConfigurationNode, IConfigurationRegistry, Extensions, IConfigurationPropertySchema, allSettings, windowSettings, resourceSettings, applicationSettings } from 'vs/platform/configuration/common/configurationRegistry';
import { createHash } from 'crypto';
import { getWorkspaceLabel, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
@@ -37,9 +37,10 @@ import { WorkspaceConfiguration, FolderConfiguration } from 'vs/workbench/servic
import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService';
import { Schemas } from 'vs/base/common/network';
import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces';
import { distinct } from 'vs/base/common/arrays';
import { UserConfiguration } from 'vs/platform/configuration/node/configuration';
import { getBaseLabel } from 'vs/base/common/labels';
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
import { localize } from 'vs/nls';
export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService {
@@ -50,7 +51,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
private defaultConfiguration: DefaultConfigurationModel;
private userConfiguration: UserConfiguration;
private workspaceConfiguration: WorkspaceConfiguration;
private cachedFolderConfigs: StrictResourceMap<FolderConfiguration>;
private cachedFolderConfigs: ResourceMap<FolderConfiguration>;
private workspaceEditingQueue: Queue<void>;
@@ -66,6 +67,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
protected readonly _onDidChangeWorkbenchState: Emitter<WorkbenchState> = this._register(new Emitter<WorkbenchState>());
public readonly onDidChangeWorkbenchState: Event<WorkbenchState> = this._onDidChangeWorkbenchState.event;
private fileService: IFileService;
private configurationEditingService: ConfigurationEditingService;
private jsonEditingService: JSONEditingService;
@@ -236,7 +238,9 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
// Workspace Configuration Service Impl
getConfigurationData(): IConfigurationData {
return this._configuration.toData();
const configurationData = this._configuration.toData();
configurationData.isComplete = this.cachedFolderConfigs.values().every(c => c.loaded);
return configurationData;
}
getValue<T>(): T;
@@ -291,32 +295,31 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
return this._configuration.keys();
}
getUnsupportedWorkspaceKeys(): string[] {
const unsupportedWorkspaceKeys = [...this.workspaceConfiguration.getUnsupportedKeys()];
for (const folder of this.workspace.folders) {
unsupportedWorkspaceKeys.push(...this.cachedFolderConfigs.get(folder.uri).getUnsupportedKeys());
}
return distinct(unsupportedWorkspaceKeys);
}
initialize(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<any> {
return this.createWorkspace(arg)
.then(workspace => this.updateWorkspaceAndInitializeConfiguration(workspace));
}
setInstantiationService(instantiationService: IInstantiationService): void {
this.configurationEditingService = instantiationService.createInstance(ConfigurationEditingService);
this.jsonEditingService = instantiationService.createInstance(JSONEditingService);
acquireFileService(fileService: IFileService): void {
this.fileService = fileService;
const changedWorkspaceFolders: IWorkspaceFolder[] = [];
TPromise.join(this.cachedFolderConfigs.values()
.map(folderConfiguration => folderConfiguration.adopt(fileService)
.then(result => {
if (result) {
changedWorkspaceFolders.push(folderConfiguration.workspaceFolder);
}
})))
.then(() => {
for (const workspaceFolder of changedWorkspaceFolders) {
this.onWorkspaceFolderConfigurationChanged(workspaceFolder);
}
});
}
handleWorkspaceFileEvents(event: FileChangesEvent): TPromise<void> {
switch (this.getWorkbenchState()) {
case WorkbenchState.FOLDER:
return this.onSingleFolderFileChanges(event);
case WorkbenchState.WORKSPACE:
return this.onWorkspaceFileChanges(event);
}
return TPromise.as(void 0);
acquireInstantiationService(instantiationService: IInstantiationService): void {
this.configurationEditingService = instantiationService.createInstance(ConfigurationEditingService);
this.jsonEditingService = instantiationService.createInstance(JSONEditingService);
}
private createWorkspace(arg: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWindowConfiguration): TPromise<Workspace> {
@@ -438,18 +441,18 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
private loadConfiguration(): TPromise<void> {
// reset caches
this.cachedFolderConfigs = new StrictResourceMap<FolderConfiguration>();
this.cachedFolderConfigs = new ResourceMap<FolderConfiguration>();
const folders = this.workspace.folders;
return this.loadFolderConfigurations(folders)
.then((folderConfigurations) => {
let workspaceConfiguration = this.getWorkspaceConfigurationModel(folderConfigurations);
const folderConfigurationModels = new StrictResourceMap<ConfigurationModel>();
const folderConfigurationModels = new ResourceMap<ConfigurationModel>();
folderConfigurations.forEach((folderConfiguration, index) => folderConfigurationModels.set(folders[index].uri, folderConfiguration));
const currentConfiguration = this._configuration;
this._configuration = new Configuration(this.defaultConfiguration, this.userConfiguration.configurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new StrictResourceMap<ConfigurationModel>(), this.getWorkbenchState() !== WorkbenchState.EMPTY ? this.workspace : null); //TODO: Sandy Avoid passing null
this._configuration = new Configuration(this.defaultConfiguration, this.userConfiguration.configurationModel, workspaceConfiguration, folderConfigurationModels, new ConfigurationModel(), new ResourceMap<ConfigurationModel>(), this.getWorkbenchState() !== WorkbenchState.EMPTY ? this.workspace : null); //TODO: Sandy Avoid passing null
if (currentConfiguration) {
const changedKeys = this._configuration.compare(currentConfiguration);
@@ -489,15 +492,29 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
private registerConfigurationSchemas(): void {
if (this.workspace) {
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
jsonRegistry.registerSchema(defaultSettingsSchemaId, settingsSchema);
jsonRegistry.registerSchema(userSettingsSchemaId, settingsSchema);
const convertToNotSuggestedProperties = (properties: IJSONSchemaMap, errorMessage: string): IJSONSchemaMap => {
return Object.keys(properties).reduce((result: IJSONSchemaMap, property) => {
result[property] = deepClone(properties[property]);
result[property].deprecationMessage = errorMessage;
return result;
}, {});
};
const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' };
const unsupportedApplicationSettings = convertToNotSuggestedProperties(applicationSettings.properties, localize('unsupportedApplicationSetting', "This setting can be applied only in User Settings"));
const workspaceSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' };
jsonRegistry.registerSchema(defaultSettingsSchemaId, allSettingsSchema);
jsonRegistry.registerSchema(userSettingsSchemaId, allSettingsSchema);
if (WorkbenchState.WORKSPACE === this.getWorkbenchState()) {
jsonRegistry.registerSchema(workspaceSettingsSchemaId, settingsSchema);
jsonRegistry.registerSchema(folderSettingsSchemaId, resourceSettingsSchema);
const unsupportedWindowSettings = convertToNotSuggestedProperties(windowSettings.properties, localize('unsupportedWindowSetting', "This setting cannot be applied now. It will be applied when you open this folder directly."));
const folderSettingsSchema: IJSONSchema = { properties: { ...unsupportedApplicationSettings, ...unsupportedWindowSettings, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: false, errorMessage: 'Unknown configuration setting' };
jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema);
jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema);
} else {
jsonRegistry.registerSchema(workspaceSettingsSchemaId, settingsSchema);
jsonRegistry.registerSchema(folderSettingsSchemaId, settingsSchema);
jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema);
jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema);
}
}
}
@@ -526,33 +543,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
return TPromise.as(null);
}
private onWorkspaceFileChanges(event: FileChangesEvent): TPromise<void> {
return TPromise.join(this.workspace.folders.map(folder =>
// handle file event for each folder
this.cachedFolderConfigs.get(folder.uri).handleWorkspaceFileEvents(event)
// Update folder configuration if handled
.then(folderConfiguration => folderConfiguration ? this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration) : new ConfigurationChangeEvent()))
).then(changeEvents => {
const consolidateChangeEvent = changeEvents.reduce((consolidated, e) => consolidated.change(e), new ConfigurationChangeEvent());
this.triggerConfigurationChange(consolidateChangeEvent, ConfigurationTarget.WORKSPACE_FOLDER);
});
}
private onSingleFolderFileChanges(event: FileChangesEvent): TPromise<void> {
const folder = this.workspace.folders[0];
return this.cachedFolderConfigs.get(folder.uri).handleWorkspaceFileEvents(event)
.then(folderConfiguration => {
if (folderConfiguration) {
// File change handled
this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration);
const workspaceChangedKeys = this._configuration.compareAndUpdateWorkspaceConfiguration(folderConfiguration);
this.triggerConfigurationChange(workspaceChangedKeys, ConfigurationTarget.WORKSPACE);
}
});
}
private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder, key?: string): TPromise<void> {
this.disposeFolderConfiguration(folder);
return this.loadFolderConfigurations([folder])
.then(([folderConfiguration]) => {
const folderChangedKeys = this._configuration.compareAndUpdateFolderConfiguration(folder.uri, folderConfiguration);
@@ -571,6 +562,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
// Remove the configurations of deleted folders
for (const key of this.cachedFolderConfigs.keys()) {
if (!this.workspace.folders.filter(folder => folder.uri.toString() === key.toString())[0]) {
const folderConfiguration = this.cachedFolderConfigs.get(key);
folderConfiguration.dispose();
this.cachedFolderConfigs.delete(key);
changeEvent = changeEvent.change(this._configuration.compareAndDeleteFolderConfiguration(key));
}
@@ -591,8 +584,12 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
private loadFolderConfigurations(folders: IWorkspaceFolder[]): TPromise<ConfigurationModel[]> {
return TPromise.join([...folders.map(folder => {
const folderConfiguration = new FolderConfiguration(folder.uri, this.workspaceSettingsRootFolder, this.getWorkbenchState());
this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration));
let folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
if (!folderConfiguration) {
folderConfiguration = new FolderConfiguration(folder, this.workspaceSettingsRootFolder, this.getWorkbenchState(), this.environmentService, this.fileService);
this._register(folderConfiguration.onDidChange(() => this.onWorkspaceFolderConfigurationChanged(folder)));
this.cachedFolderConfigs.set(folder.uri, this._register(folderConfiguration));
}
return folderConfiguration.loadConfiguration();
})]);
}
@@ -679,13 +676,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
return path1 === path2;
}
private disposeFolderConfiguration(folder: IWorkspaceFolder): void {
const folderConfiguration = this.cachedFolderConfigs.get(folder.uri);
if (folderConfiguration) {
folderConfiguration.dispose();
}
}
}
interface IExportedConfigurationNode {

View File

@@ -13,12 +13,12 @@ import URI from 'vs/base/common/uri';
import { ConfigurationChangeEvent, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { StrictResourceMap } from 'vs/base/common/map';
import { ResourceMap } from 'vs/base/common/map';
suite('FolderSettingsModelParser', () => {
suiteSetup(() => {
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': 'FolderSettingsModelParser_1',
'type': 'object',
@@ -32,47 +32,47 @@ suite('FolderSettingsModelParser', () => {
'default': 'isSet',
scope: ConfigurationScope.RESOURCE
},
'FolderSettingsModelParser.executable': {
'FolderSettingsModelParser.application': {
'type': 'string',
'default': 'isSet',
isExecutable: true
scope: ConfigurationScope.APPLICATION
}
}
});
});
test('parse all folder settings', () => {
const testObject = new FolderSettingsModelParser('settings');
const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]);
testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.executable': 'executable' }));
testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' }));
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'window': 'window', 'resource': 'resource' } });
});
test('parse resource folder settings', () => {
const testObject = new FolderSettingsModelParser('settings', ConfigurationScope.RESOURCE);
const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE]);
testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.executable': 'executable' }));
testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.window': 'window', 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.application': 'executable' }));
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource' } });
});
test('reprocess folder settings excludes executable', () => {
const testObject = new FolderSettingsModelParser('settings');
test('reprocess folder settings excludes application setting', () => {
const testObject = new FolderSettingsModelParser('settings', [ConfigurationScope.RESOURCE, ConfigurationScope.WINDOW]);
testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.anotherExecutable': 'executable' }));
testObject.parse(JSON.stringify({ 'FolderSettingsModelParser.resource': 'resource', 'FolderSettingsModelParser.anotherApplicationSetting': 'executable' }));
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource', 'anotherExecutable': 'executable' } });
assert.deepEqual(testObject.configurationModel.contents, { 'FolderSettingsModelParser': { 'resource': 'resource', 'anotherApplicationSetting': 'executable' } });
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': 'FolderSettingsModelParser_2',
'type': 'object',
'properties': {
'FolderSettingsModelParser.anotherExecutable': {
'FolderSettingsModelParser.anotherApplicationSetting': {
'type': 'string',
'default': 'isSet',
isExecutable: true
scope: ConfigurationScope.APPLICATION
}
}
});
@@ -194,7 +194,7 @@ suite('AllKeysConfigurationChangeEvent', () => {
test('changeEvent affects keys for any resource', () => {
const configuraiton = new Configuration(new ConfigurationModel({}, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']),
new ConfigurationModel(), new ConfigurationModel(), new StrictResourceMap(), new ConfigurationModel(), new StrictResourceMap(), null);
new ConfigurationModel(), new ConfigurationModel(), new ResourceMap(), new ConfigurationModel(), new ResourceMap(), null);
let testObject = new AllKeysConfigurationChangeEvent(configuraiton, ConfigurationTarget.USER, null);
assert.deepEqual(testObject.affectedKeys, ['window.title', 'window.zoomLevel', 'window.restoreFullscreen', 'workbench.editor.enablePreview', 'window.restoreWindows']);
@@ -234,4 +234,4 @@ suite('AllKeysConfigurationChangeEvent', () => {
assert.ok(!testObject.affectsConfiguration('files'));
assert.ok(!testObject.affectsConfiguration('files', URI.file('file1')));
});
});
});

View File

@@ -6,10 +6,10 @@
'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 assert from 'assert';
import * as os from 'os';
import * as path from 'path';
import * as fs from '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';
@@ -17,12 +17,13 @@ import { ParsedArgs, IEnvironmentService } from 'vs/platform/environment/common/
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, TestTextResourceConfigurationService, workbenchInstantiationService, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices';
import uuid = require('vs/base/common/uuid');
import * as extfs from 'vs/base/node/extfs';
import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import * as uuid from 'vs/base/common/uuid';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { FileService } from 'vs/workbench/services/files/electron-browser/fileService';
import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { IFileService } from 'vs/platform/files/common/files';
import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration';
@@ -34,7 +35,9 @@ import { TextModelResolverService } from 'vs/workbench/services/textmodelResolve
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { mkdirp } from 'vs/base/node/pfs';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { CommandService } from 'vs/workbench/services/commands/common/commandService';
class SettingsTestEnvironmentService extends EnvironmentService {
@@ -55,7 +58,7 @@ suite('ConfigurationEditingService', () => {
let workspaceSettingsDir;
suiteSetup(() => {
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigurationExtensions.Configuration);
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
configurationRegistry.registerConfiguration({
'id': '_test',
'type': 'object',
@@ -103,9 +106,10 @@ suite('ConfigurationEditingService', () => {
instantiationService.stub(IWorkspaceContextService, workspaceService);
return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : workspaceDir).then(() => {
instantiationService.stub(IConfigurationService, workspaceService);
instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true }));
instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
instantiationService.stub(ICommandService, CommandService);
testObject = instantiationService.createInstance(ConfigurationEditingService);
});
}
@@ -138,34 +142,34 @@ suite('ConfigurationEditingService', () => {
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));
(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));
(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));
(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));
(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));
(error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY));
});
test('dirty error is not thrown if not asked to save', () => {
@@ -177,13 +181,13 @@ suite('ConfigurationEditingService', () => {
test('do not notify error', () => {
instantiationService.stub(ITextFileService, 'isDirty', true);
const target = sinon.stub();
instantiationService.stubPromise(IChoiceService, 'choose', target);
instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: null, notify: null, error: null, info: null, warn: null });
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);
});
(error: ConfigurationEditingError) => {
assert.equal(false, target.calledOnce);
assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY);
});
});
test('write one setting - empty file', () => {

View File

@@ -16,16 +16,17 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { ParsedArgs, IEnvironmentService } 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 * as pfs from 'vs/base/node/pfs';
import * as uuid from 'vs/base/common/uuid';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService';
import { ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/node/configurationEditingService';
import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { ConfigurationTarget, IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestLifecycleService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { FileService } from 'vs/workbench/services/files/electron-browser/fileService';
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';
@@ -34,7 +35,6 @@ import { IJSONEditingService } from 'vs/workbench/services/configuration/common/
import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { mkdirp } from 'vs/base/node/pfs';
class SettingsTestEnvironmentService extends EnvironmentService {
@@ -55,7 +55,7 @@ function setUpFolder(folderName: string, parentDir: string): TPromise<string> {
const folderDir = path.join(parentDir, folderName);
// {{SQL CARBON EDIT}}
const workspaceSettingsDir = path.join(folderDir, '.sqlops');
return mkdirp(workspaceSettingsDir, 493).then(() => folderDir);
return pfs.mkdirp(workspaceSettingsDir, 493).then(() => folderDir);
}
function setUpWorkspace(folders: string[]): TPromise<{ parentDir: string, configPath: string }> {
@@ -63,7 +63,7 @@ function setUpWorkspace(folders: string[]): TPromise<{ parentDir: string, config
const id = uuid.generateUuid();
const parentDir = path.join(os.tmpdir(), 'vsctests', id);
return mkdirp(parentDir, 493)
return pfs.mkdirp(parentDir, 493)
.then(() => {
const configPath = path.join(parentDir, 'vsctests.code-workspace');
const workspace = { folders: folders.map(path => ({ path })) };

View File

@@ -16,6 +16,6 @@ export interface IConfigurationResolverService {
resolve(root: IWorkspaceFolder, value: string): string;
resolve(root: IWorkspaceFolder, value: string[]): string[];
resolve(root: IWorkspaceFolder, value: IStringDictionary<string>): IStringDictionary<string>;
resolveAny<T>(root: IWorkspaceFolder, value: T): T;
resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string }): TPromise<any>;
resolveAny<T>(root: IWorkspaceFolder, value: T, commandMapping?: IStringDictionary<string>): T;
executeCommandVariables(value: any, variables: IStringDictionary<string>): TPromise<IStringDictionary<string>>;
}

View File

@@ -3,10 +3,12 @@
* 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 { Schemas } from 'vs/base/common/network';
import { TPromise } from 'vs/base/common/winjs.base';
import { sequence } from 'vs/base/common/async';
import { toResource } from 'vs/workbench/common/editor';
import { IStringDictionary } from 'vs/base/common/collections';
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -14,159 +16,11 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { toResource } from 'vs/workbench/common/editor';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { VariableResolver } from 'vs/workbench/services/configurationResolver/node/variableResolver';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { relative } from 'path';
import { IProcessEnvironment, isWindows } from 'vs/base/common/platform';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { Schemas } from 'vs/base/common/network';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
class VariableResolver {
static VARIABLE_REGEXP = /\$\{(.*?)\}/g;
private envVariables: IProcessEnvironment;
constructor(
envVariables: IProcessEnvironment,
private configurationService: IConfigurationService,
private editorService: IWorkbenchEditorService,
private environmentService: IEnvironmentService,
private workspaceContextService: IWorkspaceContextService
) {
if (isWindows) {
this.envVariables = Object.create(null);
Object.keys(envVariables).forEach(key => {
this.envVariables[key.toLowerCase()] = envVariables[key];
});
} else {
this.envVariables = envVariables;
}
}
resolve(context: IWorkspaceFolder, value: string): string {
const filePath = this.getFilePath();
return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => {
const parts = variable.split(':');
let sufix: string;
if (parts && parts.length > 1) {
variable = parts[0];
sufix = parts[1];
}
switch (variable) {
case 'env': {
if (sufix) {
if (isWindows) {
sufix = sufix.toLowerCase();
}
const env = this.envVariables[sufix];
if (types.isString(env)) {
return env;
}
}
}
case 'config': {
if (sufix) {
const config = this.configurationService.getValue<string>(sufix, context ? { resource: context.uri } : undefined);
if (!types.isUndefinedOrNull(config) && !types.isObject(config)) {
return config;
}
}
}
default: {
if (sufix) {
const folder = this.workspaceContextService.getWorkspace().folders.filter(f => f.name === sufix).pop();
if (folder) {
context = folder;
}
}
switch (variable) {
case 'workspaceRoot':
case 'workspaceFolder':
return context ? normalizeDriveLetter(context.uri.fsPath) : match;
case 'cwd':
return context ? normalizeDriveLetter(context.uri.fsPath) : process.cwd();
case 'workspaceRootFolderName':
case 'workspaceFolderBasename':
return context ? paths.basename(context.uri.fsPath) : match;
case 'lineNumber':
return this.getLineNumber() || match;
case 'selectedText':
return this.getSelectedText() || match;
case 'file':
return filePath || match;
case 'relativeFile':
return context ? paths.normalize(relative(context.uri.fsPath, filePath)) : filePath || match;
case 'fileDirname':
return filePath ? paths.dirname(filePath) : match;
case 'fileExtname':
return filePath ? paths.extname(filePath) : match;
case 'fileBasename':
return filePath ? paths.basename(filePath) : match;
case 'fileBasenameNoExtension': {
if (!filePath) {
return match;
}
const basename = paths.basename(filePath);
return basename.slice(0, basename.length - paths.extname(basename).length);
}
case 'execPath':
return this.environmentService.execPath;
default:
return match;
}
}
}
});
}
private getSelectedText(): string {
const activeEditor = this.editorService.getActiveEditor();
if (activeEditor) {
const editorControl = (<ICodeEditor>activeEditor.getControl());
if (editorControl) {
const editorModel = editorControl.getModel();
const editorSelection = editorControl.getSelection();
if (editorModel && editorSelection) {
return editorModel.getValueInRange(editorSelection);
}
}
}
return undefined;
}
private getFilePath(): string {
let input = this.editorService.getActiveEditorInput();
if (input instanceof DiffEditorInput) {
input = input.modifiedInput;
}
const fileResource = toResource(input, { filter: Schemas.file });
if (!fileResource) {
return undefined;
}
return paths.normalize(fileResource.fsPath, true);
}
private getLineNumber(): string {
const activeEditor = this.editorService.getActiveEditor();
if (activeEditor) {
const editorControl = (<ICodeEditor>activeEditor.getControl());
if (editorControl) {
const lineNumber = editorControl.getSelection().positionLineNumber;
return String(lineNumber);
}
}
return undefined;
}
}
export class ConfigurationResolverService implements IConfigurationResolverService {
_serviceBrand: any;
@@ -180,78 +34,112 @@ export class ConfigurationResolverService implements IConfigurationResolverServi
@ICommandService private commandService: ICommandService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService
) {
this.resolver = new VariableResolver(envVariables, configurationService, editorService, environmentService, workspaceContextService);
this.resolver = new VariableResolver({
getFolderUri: (folderName: string): uri => {
const folder = workspaceContextService.getWorkspace().folders.filter(f => f.name === folderName).pop();
return folder ? folder.uri : undefined;
},
getWorkspaceFolderCount: (): number => {
return workspaceContextService.getWorkspace().folders.length;
},
getConfigurationValue: (folderUri: uri, suffix: string) => {
return configurationService.getValue<string>(suffix, folderUri ? { resource: folderUri } : undefined);
},
getExecPath: () => {
return environmentService['execPath'];
},
getFilePath: (): string | undefined => {
let input = editorService.getActiveEditorInput();
if (input instanceof DiffEditorInput) {
input = input.modifiedInput;
}
const fileResource = toResource(input, { filter: Schemas.file });
if (!fileResource) {
return undefined;
}
return paths.normalize(fileResource.fsPath, true);
},
getSelectedText: (): string | undefined => {
const activeEditor = editorService.getActiveEditor();
if (activeEditor) {
const editorControl = (<ICodeEditor>activeEditor.getControl());
if (editorControl) {
const editorModel = editorControl.getModel();
const editorSelection = editorControl.getSelection();
if (editorModel && editorSelection) {
return editorModel.getValueInRange(editorSelection);
}
}
}
return undefined;
},
getLineNumber: (): string => {
const activeEditor = editorService.getActiveEditor();
if (activeEditor) {
const editorControl = (<ICodeEditor>activeEditor.getControl());
if (editorControl) {
const lineNumber = editorControl.getSelection().positionLineNumber;
return String(lineNumber);
}
}
return undefined;
}
}, envVariables);
}
public resolve(root: IWorkspaceFolder, value: string): string;
public resolve(root: IWorkspaceFolder, value: string[]): string[];
public resolve(root: IWorkspaceFolder, value: IStringDictionary<string>): IStringDictionary<string>;
public resolve(root: IWorkspaceFolder, value: any): any {
if (types.isString(value)) {
return this.resolver.resolve(root, value);
} else if (types.isArray(value)) {
return value.map(s => this.resolver.resolve(root, s));
} else if (types.isObject(value)) {
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
Object.keys(value).forEach(key => {
result[key] = this.resolve(root, value[key]);
});
return result;
}
return value;
return this.resolver.resolveAny(root ? root.uri : undefined, value);
}
public resolveAny(root: IWorkspaceFolder, value: any): any {
if (types.isString(value)) {
return this.resolver.resolve(root, value);
} else if (types.isArray(value)) {
return value.map(s => this.resolveAny(root, s));
} else if (types.isObject(value)) {
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
Object.keys(value).forEach(key => {
result[key] = this.resolveAny(root, value[key]);
});
return result;
}
return value;
public resolveAny(root: IWorkspaceFolder, value: any, commandValueMapping?: IStringDictionary<string>): any {
return this.resolver.resolveAny(root ? root.uri : undefined, value, commandValueMapping);
}
/**
* Resolve all interactive variables in configuration #6569
* Finds and executes all command variables (see #6569)
*/
public resolveInteractiveVariables(configuration: any, interactiveVariablesMap: { [key: string]: string }): TPromise<any> {
public executeCommandVariables(configuration: any, variableToCommandMap: IStringDictionary<string>): TPromise<IStringDictionary<string>> {
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 }[] } = Object.create(null);
const findInteractiveVariables = (object: any) => {
// use an array to preserve order of first appearance
const commands: string[] = [];
const cmd_var = /\${command:(.*?)}/g;
const findCommandVariables = (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] = [];
const value = object[key];
if (value && typeof value === 'object') {
findCommandVariables(value);
} else if (typeof value === 'string') {
let matches;
while ((matches = cmd_var.exec(value)) !== null) {
if (matches.length === 2) {
const command = matches[1];
if (commands.indexOf(command) < 0) {
commands.push(command);
}
}
interactiveVariablesToSubstitutes[interactiveVariable].push({ object, key });
}
}
});
};
findInteractiveVariables(configuration);
let substitionCanceled = false;
const factory: { (): TPromise<any> }[] = Object.keys(interactiveVariablesToSubstitutes).map(interactiveVariable => {
findCommandVariables(configuration);
let cancelled = false;
const commandValueMapping: IStringDictionary<string> = Object.create(null);
const factory: { (): TPromise<any> }[] = commands.map(interactiveVariable => {
return () => {
let commandId: string = null;
commandId = interactiveVariablesMap ? interactiveVariablesMap[interactiveVariable] : null;
let commandId = variableToCommandMap ? variableToCommandMap[interactiveVariable] : null;
if (!commandId) {
// Just launch any command if the interactive variable is not contributed by the adapter #12735
commandId = interactiveVariable;
@@ -259,18 +147,14 @@ export class ConfigurationResolverService implements IConfigurationResolverServi
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);
}
});
commandValueMapping[interactiveVariable] = result;
} else {
substitionCanceled = true;
cancelled = true;
}
});
};
});
return sequence(factory).then(() => substitionCanceled ? null : configuration);
return sequence(factory).then(() => cancelled ? null : commandValueMapping);
}
}

View File

@@ -0,0 +1,218 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as paths from 'vs/base/common/paths';
import * as types from 'vs/base/common/types';
import { IStringDictionary } from 'vs/base/common/collections';
import { relative } from 'path';
import { IProcessEnvironment, isWindows } from 'vs/base/common/platform';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { localize } from 'vs/nls';
import uri from 'vs/base/common/uri';
export interface IVariableAccessor {
getFolderUri(folderName: string): uri | undefined;
getWorkspaceFolderCount(): number;
getConfigurationValue(folderUri: uri, section: string): string | undefined;
getExecPath(): string | undefined;
getFilePath(): string | undefined;
getSelectedText(): string | undefined;
getLineNumber(): string;
}
export class VariableResolver {
static VARIABLE_REGEXP = /\$\{(.*?)\}/g;
private envVariables: IProcessEnvironment;
constructor(
private accessor: IVariableAccessor,
envVariables: IProcessEnvironment
) {
if (isWindows) {
this.envVariables = Object.create(null);
Object.keys(envVariables).forEach(key => {
this.envVariables[key.toLowerCase()] = envVariables[key];
});
} else {
this.envVariables = envVariables;
}
}
resolveAny(folderUri: uri, value: any, commandValueMapping?: IStringDictionary<string>): any {
if (types.isString(value)) {
return this.resolve(folderUri, value, commandValueMapping);
} else if (types.isArray(value)) {
return value.map(s => this.resolveAny(folderUri, s, commandValueMapping));
} else if (types.isObject(value)) {
let result: IStringDictionary<string | IStringDictionary<string> | string[]> = Object.create(null);
Object.keys(value).forEach(key => {
result[key] = this.resolveAny(folderUri, value[key], commandValueMapping);
});
return result;
}
return value;
}
resolve(folderUri: uri, value: string, commandValueMapping: IStringDictionary<string>): string {
const filePath = this.accessor.getFilePath();
return value.replace(VariableResolver.VARIABLE_REGEXP, (match: string, variable: string) => {
let argument: string;
const parts = variable.split(':');
if (parts && parts.length > 1) {
variable = parts[0];
argument = parts[1];
}
switch (variable) {
case 'env':
if (argument) {
if (isWindows) {
argument = argument.toLowerCase();
}
const env = this.envVariables[argument];
if (types.isString(env)) {
return env;
}
// For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436
return '';
}
throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable name is given.", match));
case 'config':
if (argument) {
const config = this.accessor.getConfigurationValue(folderUri, argument);
if (types.isUndefinedOrNull(config)) {
throw new Error(localize('configNotFound', "'{0}' can not be resolved because setting '{1}' not found.", match, argument));
}
if (types.isObject(config)) {
throw new Error(localize('configNoString', "'{0}' can not be resolved because '{1}' is a structured value.", match, argument));
}
return config;
}
throw new Error(localize('missingConfigName', "'{0}' can not be resolved because no settings name is given.", match));
case 'command':
if (argument && commandValueMapping) {
const v = commandValueMapping[argument];
if (typeof v === 'string') {
return v;
}
throw new Error(localize('noValueForCommand', "'{0}' can not be resolved because the command has no value.", match));
}
return match;
default: {
// common error handling for all variables that require an open folder and accept a folder name argument
switch (variable) {
case 'workspaceRoot':
case 'workspaceFolder':
case 'workspaceRootFolderName':
case 'workspaceFolderBasename':
case 'relativeFile':
if (argument) {
const folder = this.accessor.getFolderUri(argument);
if (folder) {
folderUri = folder;
} else {
throw new Error(localize('canNotFindFolder', "'{0}' can not be resolved. No such folder '{1}'.", match, argument));
}
}
if (!folderUri) {
if (this.accessor.getWorkspaceFolderCount() > 1) {
throw new Error(localize('canNotResolveWorkspaceFolderMultiRoot', "'{0}' can not be resolved in a multi folder workspace. Scope this variable using ':' and a workspace folder name.", match));
}
throw new Error(localize('canNotResolveWorkspaceFolder', "'{0}' can not be resolved. Please open a folder.", match));
}
break;
default:
break;
}
// common error handling for all variables that require an open file
switch (variable) {
case 'file':
case 'relativeFile':
case 'fileDirname':
case 'fileExtname':
case 'fileBasename':
case 'fileBasenameNoExtension':
if (!filePath) {
throw new Error(localize('canNotResolveFile', "'{0}' can not be resolved. Please open an editor.", match));
}
break;
default:
break;
}
switch (variable) {
case 'workspaceRoot':
case 'workspaceFolder':
return normalizeDriveLetter(folderUri.fsPath);
case 'cwd':
return folderUri ? normalizeDriveLetter(folderUri.fsPath) : process.cwd();
case 'workspaceRootFolderName':
case 'workspaceFolderBasename':
return paths.basename(folderUri.fsPath);
case 'lineNumber':
const lineNumber = this.accessor.getLineNumber();
if (lineNumber) {
return lineNumber;
}
throw new Error(localize('canNotResolveLineNumber', "'{0}' can not be resolved. Make sure to have a line selected in the active editor.", match));
case 'selectedText':
const selectedText = this.accessor.getSelectedText();
if (selectedText) {
return selectedText;
}
throw new Error(localize('canNotResolveSelectedText', "'{0}' can not be resolved. Make sure to have some text selected in the active editor.", match));
case 'file':
return filePath;
case 'relativeFile':
if (folderUri) {
return paths.normalize(relative(folderUri.fsPath, filePath));
}
return filePath;
case 'fileDirname':
return paths.dirname(filePath);
case 'fileExtname':
return paths.extname(filePath);
case 'fileBasename':
return paths.basename(filePath);
case 'fileBasenameNoExtension':
const basename = paths.basename(filePath);
return basename.slice(0, basename.length - paths.extname(basename).length);
case 'execPath':
const ep = this.accessor.getExecPath();
if (ep) {
return ep;
}
return match;
default:
return match;
}
}
}
});
}
}

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert = require('assert');
import * as assert from 'assert';
import uri from 'vs/base/common/uri';
import platform = require('vs/base/common/platform');
import * as platform from 'vs/base/common/platform';
import { TPromise } from 'vs/base/common/winjs.base';
import { IConfigurationService, getConfigurationValue, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
import { ICommandService } from 'vs/platform/commands/common/commands';
@@ -86,7 +86,7 @@ suite('Configuration Resolver Service', () => {
if (platform.isWindows) {
assert.strictEqual(configurationResolverService.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - Value for key1');
} else {
assert.strictEqual(configurationResolverService.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - ${env:Key1}');
assert.strictEqual(configurationResolverService.resolve(workspace, '${env:key1} - ${env:Key1}'), 'Value for key1 - ');
}
});
@@ -196,18 +196,6 @@ suite('Configuration Resolver Service', () => {
assert.strictEqual(service.resolve(workspace, '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, new TestContextService());
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor[\'abc\'.substr(0)]} xyz'), 'abc ${config:editor[\'abc\'.substr(0)]} xyz');
});
test('uses original variable as fallback', () => {
let configurationService: IConfigurationService;
configurationService = new MockConfigurationService({
@@ -215,10 +203,8 @@ suite('Configuration Resolver Service', () => {
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService());
assert.strictEqual(service.resolve(workspace, 'abc ${invalidVariable} xyz'), 'abc ${invalidVariable} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${env:invalidVariable} xyz'), 'abc ${env:invalidVariable} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.abc.def} xyz'), 'abc ${config:editor.abc.def} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${config:panel.abc} xyz'), 'abc ${config:panel.abc} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${unknownVariable} xyz'), 'abc ${unknownVariable} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${env:unknownVariable} xyz'), 'abc xyz');
});
test('configuration variables with invalid accessor', () => {
@@ -230,9 +216,14 @@ suite('Configuration Resolver Service', () => {
});
let service = new ConfigurationResolverService(envVariables, new TestEditorService(), TestEnvironmentService, configurationService, mockCommandService, new TestContextService());
assert.strictEqual(service.resolve(workspace, 'abc ${config:} xyz'), 'abc ${config:} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor..fontFamily} xyz'), 'abc ${config:editor..fontFamily} xyz');
assert.strictEqual(service.resolve(workspace, 'abc ${config:editor.none.none2} xyz'), 'abc ${config:editor.none.none2} xyz');
assert.throws(() => service.resolve(workspace, 'abc ${env} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${env:} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config:} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config:editor} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config:editor..fontFamily} xyz'));
assert.throws(() => service.resolve(workspace, 'abc ${config:editor.none.none2} xyz'));
});
test('interactive variable simple', () => {
@@ -249,8 +240,11 @@ suite('Configuration Resolver Service', () => {
interactiveVariables['interactiveVariable1'] = 'command1';
interactiveVariables['interactiveVariable2'] = 'command2';
configurationResolverService.resolveInteractiveVariables(configuration, interactiveVariables).then(resolved => {
assert.deepEqual(resolved, {
configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => {
const result = configurationResolverService.resolveAny(undefined, configuration, mapping);
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',
@@ -281,8 +275,11 @@ suite('Configuration Resolver Service', () => {
interactiveVariables['interactiveVariable1'] = 'command1';
interactiveVariables['interactiveVariable2'] = 'command2';
configurationResolverService.resolveInteractiveVariables(configuration, interactiveVariables).then(resolved => {
assert.deepEqual(resolved, {
configurationResolverService.executeCommandVariables(configuration, interactiveVariables).then(mapping => {
const result = configurationResolverService.resolveAny(undefined, configuration, mapping);
assert.deepEqual(result, {
'name': 'Attach to Process',
'type': 'node',
'request': 'attach',

View File

@@ -8,14 +8,14 @@
import { TPromise } from 'vs/base/common/winjs.base';
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 * as dom from 'vs/base/browser/dom';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { remote, webFrame } from 'electron';
import { unmnemonicLabel } from 'vs/base/common/labels';
import Event, { Emitter } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IContextMenuDelegate, ContextSubMenu, IEvent } from 'vs/base/browser/contextmenu';

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import * as nls from 'vs/nls';
import { onUnexpectedError } from 'vs/base/common/errors';
import { assign, deepClone } from 'vs/base/common/objects';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -27,7 +27,7 @@ export interface ICrashReporterConfig {
enableCrashReporter: boolean;
}
const configurationRegistry = <IConfigurationRegistry>Registry.as(Extensions.Configuration);
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
configurationRegistry.registerConfiguration({
'id': TELEMETRY_SECTION_ID,
'order': 110,
@@ -119,7 +119,7 @@ export class CrashReporterService implements ICrashReporterService {
// Experimental crash reporting support for child processes on Mac only for now
if (this.isEnabled && isMacintosh) {
const childProcessOptions = deepClone(this.options);
childProcessOptions.extra.processName = name;
(<any>childProcessOptions.extra).processName = name;
childProcessOptions.crashesDirectory = os.tmpdir();
return childProcessOptions;

View File

@@ -6,9 +6,10 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import URI from 'vs/base/common/uri';
import Event from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { IDisposable } from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
export const IDecorationsService = createDecorator<IDecorationsService>('IFileDecorationsService');
@@ -31,7 +32,7 @@ export interface IDecoration {
export interface IDecorationsProvider {
readonly label: string;
readonly onDidChange: Event<URI[]>;
provideDecorations(uri: URI): IDecorationData | Thenable<IDecorationData>;
provideDecorations(uri: URI): IDecorationData | TPromise<IDecorationData>;
}
export interface IResourceDecorationChangeEvent {

View File

@@ -5,7 +5,7 @@
'use strict';
import URI from 'vs/base/common/uri';
import Event, { Emitter, debounceEvent, anyEvent } from 'vs/base/common/event';
import { Event, Emitter, debounceEvent, anyEvent } from 'vs/base/common/event';
import { IDecorationsService, IDecoration, IResourceDecorationChangeEvent, IDecorationsProvider, IDecorationData } from './decorations';
import { TernarySearchTree } from 'vs/base/common/map';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
@@ -17,6 +17,8 @@ import { IdGenerator } from 'vs/base/common/idGenerator';
import { IIterator } from 'vs/base/common/iterator';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { localize } from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { isPromiseCanceledError } from 'vs/base/common/errors';
class DecorationRule {
@@ -229,7 +231,7 @@ class FileDecorationChangeEvent implements IResourceDecorationChangeEvent {
class DecorationProviderWrapper {
readonly data = TernarySearchTree.forPaths<Thenable<void> | IDecorationData>();
readonly data = TernarySearchTree.forPaths<TPromise<void> | IDecorationData>();
private readonly _dispoable: IDisposable;
constructor(
@@ -245,6 +247,9 @@ class DecorationProviderWrapper {
} else {
// selective changes -> drop for resource, fetch again, send event
// perf: the map stores thenables, decorations, or `null`-markers.
// we make us of that and ignore all uris in which we have never
// been interested.
for (const uri of uris) {
this._fetchData(uri);
}
@@ -295,6 +300,13 @@ class DecorationProviderWrapper {
private _fetchData(uri: URI): IDecorationData {
// check for pending request and cancel it
const pendingRequest = this.data.get(uri.toString());
if (TPromise.is(pendingRequest)) {
pendingRequest.cancel();
this.data.delete(uri.toString());
}
const dataOrThenable = this._provider.provideDecorations(uri);
if (!isThenable(dataOrThenable)) {
// sync -> we have a result now
@@ -302,10 +314,17 @@ class DecorationProviderWrapper {
} else {
// async -> we have a result soon
const request = Promise.resolve(dataOrThenable)
.then(data => this._keepItem(uri, data))
.catch(_ => this.data.delete(uri.toString()));
const request = TPromise.wrap(dataOrThenable).then(data => {
if (this.data.get(uri.toString()) === request) {
this._keepItem(uri, data);
}
}, err => {
if (!isPromiseCanceledError(err) && this.data.get(uri.toString()) === request) {
this.data.delete(uri.toString());
}
});
// {{ SQL CARBON EDIT }} - Add type assertion to fix build break
this.data.set(uri.toString(), <any>request);
return undefined;
}
@@ -363,6 +382,8 @@ export class FileDecorationsService implements IDecorationsService {
dispose(): void {
dispose(this._disposables);
dispose(this._onDidChangeDecorations);
dispose(this._onDidChangeDecorationsDelayed);
}
registerDecorationsProvider(provider: IDecorationsProvider): IDisposable {

View File

@@ -9,8 +9,9 @@ import * as assert from 'assert';
import { FileDecorationsService } from 'vs/workbench/services/decorations/browser/decorationsService';
import { IDecorationsProvider, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations';
import URI from 'vs/base/common/uri';
import Event, { toPromise } from 'vs/base/common/event';
import { Event, toPromise, Emitter } from 'vs/base/common/event';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
import { TPromise } from 'vs/base/common/winjs.base';
suite('DecorationsService', function () {
@@ -33,7 +34,7 @@ suite('DecorationsService', function () {
readonly onDidChange: Event<URI[]> = Event.None;
provideDecorations(uri: URI) {
callCounter += 1;
return new Promise<IDecorationData>(resolve => {
return new TPromise<IDecorationData>(resolve => {
setTimeout(() => resolve({
color: 'someBlue',
tooltip: 'T'
@@ -75,7 +76,7 @@ suite('DecorationsService', function () {
assert.equal(callCounter, 1);
});
test('Clear decorations on provider dispose', function () {
test('Clear decorations on provider dispose', async function () {
let uri = URI.parse('foo:bar');
let callCounter = 0;
@@ -94,13 +95,14 @@ suite('DecorationsService', function () {
// un-register -> ensure good event
let didSeeEvent = false;
service.onDidChangeDecorations(e => {
let p = toPromise(service.onDidChangeDecorations).then(e => {
assert.equal(e.affectsResource(uri), true);
assert.deepEqual(service.getDecoration(uri, false), undefined);
assert.equal(callCounter, 1);
didSeeEvent = true;
});
reg.dispose();
await p;
assert.equal(didSeeEvent, true);
});
@@ -168,4 +170,42 @@ suite('DecorationsService', function () {
reg.dispose();
});
test('Decorations not showing up for second root folder #48502', async function () {
let cancelCount = 0;
let callCount = 0;
let provider = new class implements IDecorationsProvider {
_onDidChange = new Emitter<URI[]>();
onDidChange: Event<URI[]> = this._onDidChange.event;
label: string = 'foo';
provideDecorations(uri): TPromise<IDecorationData> {
return new TPromise(resolve => {
callCount += 1;
setTimeout(() => {
resolve({ letter: 'foo' });
}, 10);
}, () => {
cancelCount += 1;
});
}
};
let reg = service.registerDecorationsProvider(provider);
const uri = URI.parse('foo://bar');
service.getDecoration(uri, false);
provider._onDidChange.fire([uri]);
service.getDecoration(uri, false);
assert.equal(cancelCount, 1);
assert.equal(callCount, 2);
reg.dispose();
});
});

View File

@@ -0,0 +1,144 @@
/*---------------------------------------------------------------------------------------------
* 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 product from 'vs/platform/node/product';
import { TPromise } from 'vs/base/common/winjs.base';
import Severity from 'vs/base/common/severity';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IDialogService, IConfirmation, IConfirmationResult, IDialogOptions } from 'vs/platform/dialogs/common/dialogs';
interface IMassagedMessageBoxOptions {
/**
* OS massaged message box options.
*/
options: Electron.MessageBoxOptions;
/**
* Since the massaged result of the message box options potentially
* changes the order of buttons, we have to keep a map of these
* changes so that we can still return the correct index to the caller.
*/
buttonIndexMap: number[];
}
export class DialogService implements IDialogService {
public _serviceBrand: any;
constructor(
@IWindowService private windowService: IWindowService
) {
}
public confirm(confirmation: IConfirmation): TPromise<IConfirmationResult> {
const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
return this.windowService.showMessageBox(options).then(result => {
return {
confirmed: buttonIndexMap[result.button] === 0 ? true : false,
checkboxChecked: result.checkboxChecked
} as IConfirmationResult;
});
}
private getConfirmOptions(confirmation: IConfirmation): Electron.MessageBoxOptions {
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"));
}
const opts: Electron.MessageBoxOptions = {
title: confirmation.title,
message: confirmation.message,
buttons,
cancelId: 1
};
if (confirmation.detail) {
opts.detail = confirmation.detail;
}
if (confirmation.type) {
opts.type = confirmation.type;
}
if (confirmation.checkbox) {
opts.checkboxLabel = confirmation.checkbox.label;
opts.checkboxChecked = confirmation.checkbox.checked;
}
return opts;
}
public show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): TPromise<number> {
const { options, buttonIndexMap } = this.massageMessageBoxOptions({
message,
buttons,
type: (severity === Severity.Info) ? 'question' : (severity === Severity.Error) ? 'error' : (severity === Severity.Warning) ? 'warning' : 'none',
cancelId: dialogOptions ? dialogOptions.cancelId : void 0,
detail: dialogOptions ? dialogOptions.detail : void 0
});
return this.windowService.showMessageBox(options).then(result => buttonIndexMap[result.button]);
}
private massageMessageBoxOptions(options: Electron.MessageBoxOptions): IMassagedMessageBoxOptions {
let buttonIndexMap = options.buttons.map((button, index) => index);
let buttons = options.buttons.map(button => mnemonicButtonLabel(button));
let cancelId = options.cancelId;
// Linux: order of buttons is reverse
// macOS: also reverse, but the OS handles this for us!
if (isLinux) {
buttons = buttons.reverse();
buttonIndexMap = buttonIndexMap.reverse();
}
// Default Button (always first one)
options.defaultId = buttonIndexMap[0];
// Cancel Button
if (typeof cancelId === 'number') {
// Ensure the cancelId is the correct one from our mapping
cancelId = buttonIndexMap[cancelId];
// macOS/Linux: the cancel button should always be to the left of the primary action
// if we see more than 2 buttons, move the cancel one to the left of the primary
if (!isWindows && buttons.length > 2 && cancelId !== 1) {
const cancelButton = buttons[cancelId];
buttons.splice(cancelId, 1);
buttons.splice(1, 0, cancelButton);
const cancelButtonIndex = buttonIndexMap[cancelId];
buttonIndexMap.splice(cancelId, 1);
buttonIndexMap.splice(1, 0, cancelButtonIndex);
cancelId = 1;
}
}
options.buttons = buttons;
options.cancelId = cancelId;
options.noLink = true;
options.title = options.title || product.nameLong;
return { options, buttonIndexMap };
}
}

View File

@@ -1,241 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 Severity from 'vs/base/common/severity';
import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { Action } from 'vs/base/common/actions';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IConfirmationService, IChoiceService, IConfirmation, IConfirmationResult, Choice } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification';
import { once } from 'vs/base/common/event';
import URI from 'vs/base/common/uri';
import { basename } from 'vs/base/common/paths';
interface IMassagedMessageBoxOptions {
/**
* OS massaged message box options.
*/
options: Electron.MessageBoxOptions;
/**
* Since the massaged result of the message box options potentially
* changes the order of buttons, we have to keep a map of these
* changes so that we can still return the correct index to the caller.
*/
buttonIndexMap: number[];
}
export class DialogService implements IChoiceService, IConfirmationService {
public _serviceBrand: any;
constructor(
@IWindowService private windowService: IWindowService,
@INotificationService private notificationService: INotificationService
) {
}
public confirmWithCheckbox(confirmation: IConfirmation): TPromise<IConfirmationResult> {
const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
return this.windowService.showMessageBox(options).then(result => {
return {
confirmed: buttonIndexMap[result.button] === 0 ? true : false,
checkboxChecked: result.checkboxChecked
} as IConfirmationResult;
});
}
public confirm(confirmation: IConfirmation): TPromise<boolean> {
const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
return this.windowService.showMessageBox(options).then(result => buttonIndexMap[result.button] === 0 ? true : false);
}
private getConfirmOptions(confirmation: IConfirmation): Electron.MessageBoxOptions {
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"));
}
const opts: Electron.MessageBoxOptions = {
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;
}
if (confirmation.checkbox) {
opts.checkboxLabel = confirmation.checkbox.label;
opts.checkboxChecked = confirmation.checkbox.checked;
}
return opts;
}
public choose(severity: Severity, message: string, choices: Choice[], cancelId?: number, modal: boolean = false): TPromise<number> {
if (modal) {
return this.doChooseWithDialog(severity, message, choices, cancelId);
}
return this.doChooseWithNotification(severity, message, choices);
}
private doChooseWithDialog(severity: Severity, message: string, choices: Choice[], cancelId?: number): TPromise<number> {
const type: 'none' | 'info' | 'error' | 'question' | 'warning' = severity === Severity.Info ? 'question' : severity === Severity.Error ? 'error' : severity === Severity.Warning ? 'warning' : 'none';
const stringChoices: string[] = [];
choices.forEach(choice => {
if (typeof choice === 'string') {
stringChoices.push(choice);
} else {
stringChoices.push(choice.label);
}
});
const { options, buttonIndexMap } = this.massageMessageBoxOptions({ message, buttons: stringChoices, type, cancelId });
return this.windowService.showMessageBox(options).then(result => buttonIndexMap[result.button]);
}
private doChooseWithNotification(severity: Severity, message: string, choices: Choice[]): TPromise<number> {
let handle: INotificationHandle;
const promise = new TPromise<number>((c, e) => {
// Complete promise with index of action that was picked
const callback = (index: number, closeNotification: boolean) => () => {
c(index);
if (closeNotification) {
handle.close();
}
return TPromise.as(void 0);
};
// Convert choices into primary/secondary actions
const actions: INotificationActions = {
primary: [],
secondary: []
};
choices.forEach((choice, index) => {
let isPrimary = true;
let label: string;
let closeNotification = false;
if (typeof choice === 'string') {
label = choice;
} else {
isPrimary = false;
label = choice.label;
closeNotification = !choice.keepOpen;
}
const action = new Action(`workbench.dialog.choice.${index}`, label, null, true, callback(index, closeNotification));
if (isPrimary) {
actions.primary.push(action);
} else {
actions.secondary.push(action);
}
});
// Show notification with actions
handle = this.notificationService.notify({ severity, message, actions });
// Cancel promise when notification gets disposed
once(handle.onDidClose)(() => promise.cancel());
}, () => handle.close());
return promise;
}
private massageMessageBoxOptions(options: Electron.MessageBoxOptions): IMassagedMessageBoxOptions {
let buttonIndexMap = options.buttons.map((button, index) => index);
options.buttons = options.buttons.map(button => mnemonicButtonLabel(button));
// Linux: order of buttons is reverse
// macOS: also reverse, but the OS handles this for us!
if (isLinux) {
options.buttons = options.buttons.reverse();
buttonIndexMap = buttonIndexMap.reverse();
}
// Default Button
if (options.defaultId !== void 0) {
options.defaultId = buttonIndexMap[options.defaultId];
} else if (isLinux) {
options.defaultId = buttonIndexMap[0];
}
// Cancel Button
if (options.cancelId !== void 0) {
// macOS: the cancel button should always be to the left of the primary action
// if we see more than 2 buttons, move the cancel one to the left of the primary
if (isMacintosh && options.buttons.length > 2 && options.cancelId !== 1) {
const cancelButton = options.buttons[options.cancelId];
options.buttons.splice(options.cancelId, 1);
options.buttons.splice(1, 0, cancelButton);
const cancelButtonIndex = buttonIndexMap[options.cancelId];
buttonIndexMap.splice(cancelButtonIndex, 1);
buttonIndexMap.splice(1, 0, cancelButtonIndex);
}
options.cancelId = buttonIndexMap[options.cancelId];
}
options.noLink = true;
options.title = options.title || product.nameLong;
return { options, buttonIndexMap };
}
}
const MAX_CONFIRM_FILES = 10;
export function getConfirmMessage(start: string, resourcesToConfirm: URI[]): string {
const message = [start];
message.push('');
message.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => basename(r.fsPath)));
if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {
if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {
message.push(nls.localize('moreFile', "...1 additional file not shown"));
} else {
message.push(nls.localize('moreFiles', "...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));
}
}
message.push('');
return message.join('\n');
}

View File

@@ -16,7 +16,7 @@ import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorIn
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import nls = require('vs/nls');
import * as nls from 'vs/nls';
import { getPathLabel } from 'vs/base/common/labels';
import { ResourceMap } from 'vs/base/common/map';
import { once } from 'vs/base/common/event';

View File

@@ -7,7 +7,7 @@
import * as assert from 'assert';
import { Promise, TPromise } from 'vs/base/common/winjs.base';
import paths = require('vs/base/common/paths');
import * as paths from 'vs/base/common/paths';
import { Position, IEditor, IEditorInput } from 'vs/platform/editor/common/editor';
import URI from 'vs/base/common/uri';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';

View File

@@ -8,7 +8,7 @@ import Severity from 'vs/base/common/severity';
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import Event from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
export interface IExtensionDescription {
readonly id: string;

View File

@@ -14,7 +14,7 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement';
const hasOwnProperty = Object.hasOwnProperty;
const schemaRegistry = <IJSONContributionRegistry>Registry.as(Extensions.JSONContribution);
const schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
export class ExtensionMessageCollector {
@@ -125,12 +125,12 @@ const schema: IJSONSchema = {
properties: {
engines: {
type: 'object',
description: nls.localize('vscode.extension.engines', "Engine compatibility."),
properties: {
'vscode': {
type: 'string',
description: nls.localize('vscode.extension.engines.vscode', 'For VS Code extensions, specifies the VS Code version that the extension is compatible with. Cannot be *. For example: ^0.10.5 indicates compatibility with a minimum VS Code version of 0.10.5.'),
default: '^0.10.0',
default: '^1.22.0',
}
}
},
@@ -147,8 +147,15 @@ const schema: IJSONSchema = {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
enum: ['Languages', 'Snippets', 'Linters', 'Themes', 'Debuggers', 'Other', 'Keymaps', 'Formatters', 'Extension Packs', 'SCM Providers', 'Azure', 'Language Packs']
oneOf: [{
type: 'string',
enum: ['Programming Languages', 'Snippets', 'Linters', 'Themes', 'Debuggers', 'Other', 'Keymaps', 'Formatters', 'Extension Packs', 'SCM Providers', 'Azure', 'Language Packs'],
},
{
type: 'string',
const: 'Languages',
deprecationMessage: nls.localize('vscode.extension.category.languages.deprecated', 'Use \'Programming Languages\' instead'),
}]
}
},
galleryBanner: {
@@ -219,6 +226,11 @@ const schema: IJSONSchema = {
body: 'onView:${5:viewId}',
description: nls.localize('vscode.extension.activationEvents.onView', 'An activation event emitted whenever the specified view is expanded.'),
},
{
label: 'onUri',
body: 'onUri',
description: nls.localize('vscode.extension.activationEvents.onUri', 'An activation event emitted whenever a system-wide Uri directed towards this extension is open.'),
},
{
label: '*',
description: nls.localize('vscode.extension.activationEvents.star', 'An activation event emitted on VS Code startup. To ensure a great end user experience, please use this activation event in your extension only when no other activation events combination works in your use-case.'),
@@ -249,6 +261,25 @@ const schema: IJSONSchema = {
}
}
},
markdown: {
type: 'string',
description: nls.localize('vscode.extension.markdown', "Controls the Markdown rendering engine used in the Marketplace. Either github (default) or standard."),
enum: ['github', 'standard'],
default: 'github'
},
qna: {
default: 'marketplace',
description: nls.localize('vscode.extension.qna', "Controls the Q&A link in the Marketplace. Set to marketplace to enable the default Marketplace Q & A site. Set to a string to provide the URL of a custom Q & A site. Set to false to disable Q & A altogether."),
anyOf: [
{
type: ['string', 'boolean'],
enum: ['marketplace', false]
},
{
type: 'string'
}
]
},
extensionDependencies: {
description: nls.localize('vscode.extension.extensionDependencies', 'Dependencies to other extensions. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp.'),
type: 'array',

View File

@@ -23,7 +23,7 @@ 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, anyEvent, fromNodeEventEmitter } from 'vs/base/common/event';
import { Event, Emitter, debounceEvent, mapEvent, anyEvent, fromNodeEventEmitter } from 'vs/base/common/event';
import { IInitData, IWorkspaceData, IConfigurationInitData } from 'vs/workbench/api/node/extHost.protocol';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
@@ -36,11 +36,10 @@ import { IRemoteConsoleLog, log, parse } from 'vs/base/node/console';
import { getScopes } from 'vs/platform/configuration/common/configurationRegistry';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs';
export class ExtensionHostProcessWorker {
private _onCrashed: Emitter<[number, string]> = new Emitter<[number, string]>();
private readonly _onCrashed: Emitter<[number, string]> = new Emitter<[number, string]>();
public readonly onCrashed: Event<[number, string]> = this._onCrashed.event;
private readonly _toDispose: IDisposable[];
@@ -73,7 +72,6 @@ export class ExtensionHostProcessWorker {
@IWorkspaceConfigurationService private readonly _configurationService: IWorkspaceConfigurationService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@ICrashReporterService private readonly _crashReporterService: ICrashReporterService,
@IChoiceService private readonly _choiceService: IChoiceService,
@ILogService private readonly _logService: ILogService
) {
// handle extension host lifecycle a bit special when we know we are developing an extension that runs inside
@@ -143,7 +141,6 @@ export class ExtensionHostProcessWorker {
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,
VSCODE_LOG_STACK: !this._isExtensionDevTestFromCli && (this._isExtensionDevHost || !this._environmentService.isBuilt || product.quality !== 'stable' || this._environmentService.verbose)
@@ -238,11 +235,12 @@ export class ExtensionHostProcessWorker {
? 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._choiceService.choose(Severity.Warning, msg, [nls.localize('reloadWindow', "Reload Window")]).then(choice => {
if (choice === 0) {
this._windowService.reloadWindow();
}
});
this._notificationService.prompt(Severity.Warning, msg,
[{
label: nls.localize('reloadWindow', "Reload Window"),
run: () => this._windowService.reloadWindow()
}]
);
}, 10000);
}
@@ -372,22 +370,17 @@ export class ExtensionHostProcessWorker {
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'] || []
extensionTestsPath: this._environmentService.extensionTestsPath
},
workspace: this._contextService.getWorkbenchState() === WorkbenchState.EMPTY ? null : <IWorkspaceData>this._contextService.getWorkspace(),
extensions: extensionDescriptions,
// Send configurations scopes only in development mode.
configuration: !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment ? { ...configurationData, configurationScopes: getScopes() } : configurationData,
telemetryInfo,
args: this._environmentService.args,
execPath: this._environmentService.execPath,
windowId: this._windowService.getCurrentWindowId(),
logLevel: this._logService.getLevel()
logLevel: this._logService.getLevel(),
logsPath: this._environmentService.logsPath
};
return r;
});

View File

@@ -17,8 +17,8 @@ import * as platform from 'vs/base/common/platform';
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry';
import { IMessage, IExtensionDescription, IExtensionsStatus, IExtensionService, ExtensionPointContribution, ActivationTimes, ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
import { USER_MANIFEST_CACHE_FILE, BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER } from 'vs/platform/extensions/common/extensions';
import { IExtensionEnablementService, IExtensionIdentifier, EnablementState } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, BetterMergeId, BetterMergeDisabledNowKey } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IExtensionEnablementService, IExtensionIdentifier, EnablementState, IExtensionManagementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, BetterMergeId, BetterMergeDisabledNowKey, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionsRegistry, ExtensionPoint, IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ExtensionScanner, ILog, ExtensionScannerInput, IExtensionResolver, IExtensionReference, Translations } from 'vs/workbench/services/extensions/node/extensionPoints';
import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier';
@@ -35,13 +35,13 @@ import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { mark, time } from 'vs/base/common/performance';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { Barrier } from 'vs/base/common/async';
import Event, { Emitter } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler';
import product from 'vs/platform/node/product';
import * as strings from 'vs/base/common/strings';
import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs';
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
let _SystemExtensionsRoot: string = null;
function getSystemExtensionsRoot(): string {
@@ -111,10 +111,160 @@ function messageWithSource2(source: string, message: string): string {
const hasOwnProperty = Object.hasOwnProperty;
const NO_OP_VOID_PROMISE = TPromise.wrap<void>(void 0);
export class ExtensionHostProcessManager extends Disposable {
public readonly onDidCrash: Event<[number, string]>;
/**
* A map of already activated events to speed things up if the same activation event is triggered multiple times.
*/
private readonly _extensionHostProcessFinishedActivateEvents: { [activationEvent: string]: boolean; };
private readonly _extensionHostProcessActivationTimes: { [id: string]: ActivationTimes; };
private readonly _extensionHostExtensionRuntimeErrors: { [id: string]: Error[]; };
private _extensionHostProcessRPCProtocol: RPCProtocol;
private readonly _extensionHostProcessCustomers: IDisposable[];
private readonly _extensionHostProcessWorker: ExtensionHostProcessWorker;
/**
* winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object.
*/
private readonly _extensionHostProcessProxy: TPromise<{ value: ExtHostExtensionServiceShape; }>;
constructor(
extensionHostProcessWorker: ExtensionHostProcessWorker,
initialActivationEvents: string[],
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
) {
super();
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
this._extensionHostProcessActivationTimes = Object.create(null);
this._extensionHostExtensionRuntimeErrors = Object.create(null);
this._extensionHostProcessRPCProtocol = null;
this._extensionHostProcessCustomers = [];
this._extensionHostProcessProxy = null;
this._extensionHostProcessWorker = extensionHostProcessWorker;
this.onDidCrash = this._extensionHostProcessWorker.onCrashed;
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));
});
}
public dispose(): void {
if (this._extensionHostProcessWorker) {
this._extensionHostProcessWorker.dispose();
}
if (this._extensionHostProcessRPCProtocol) {
this._extensionHostProcessRPCProtocol.dispose();
}
for (let i = 0, len = this._extensionHostProcessCustomers.length; i < len; i++) {
const customer = this._extensionHostProcessCustomers[i];
try {
customer.dispose();
} catch (err) {
errors.onUnexpectedError(err);
}
}
super.dispose();
}
public getActivatedExtensionIds(): string[] {
return Object.keys(this._extensionHostProcessActivationTimes);
}
public getActivationTimes(): { [id: string]: ActivationTimes; } {
return this._extensionHostProcessActivationTimes;
}
public getRuntimeErrors(): { [id: string]: Error[]; } {
return this._extensionHostExtensionRuntimeErrors;
}
public canProfileExtensionHost(): boolean {
return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort());
}
private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape {
if (logExtensionHostCommunication || this._environmentService.logExtensionHostCommunication) {
protocol = asLoggingProtocol(protocol);
}
this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol);
const extHostContext: IExtHostContext = this._extensionHostProcessRPCProtocol;
// 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._extensionHostProcessRPCProtocol.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._extensionHostProcessRPCProtocol.assertRegistered(expected);
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
}
public activateByEvent(activationEvent: string): TPromise<void> {
if (this._extensionHostProcessFinishedActivateEvents[activationEvent] || !this._extensionHostProcessProxy) {
return NO_OP_VOID_PROMISE;
}
return this._extensionHostProcessProxy.then((proxy) => {
return proxy.value.$activateByEvent(activationEvent);
}).then(() => {
this._extensionHostProcessFinishedActivateEvents[activationEvent] = true;
});
}
public startExtensionHostProfile(): TPromise<ProfileSession> {
if (this._extensionHostProcessWorker) {
let port = this._extensionHostProcessWorker.getInspectPort();
if (port) {
return this._instantiationService.createInstance(ExtensionHostProfiler, port).start();
}
}
throw new Error('Extension host not running or no inspect port available');
}
public onExtensionActivated(extensionId: string, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
this._extensionHostProcessActivationTimes[extensionId] = new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent);
}
public onExtensionRuntimeError(extensionId: string, err: Error): void {
if (!this._extensionHostExtensionRuntimeErrors[extensionId]) {
this._extensionHostExtensionRuntimeErrors[extensionId] = [];
}
this._extensionHostExtensionRuntimeErrors[extensionId].push(err);
}
}
export class ExtensionService extends Disposable implements IExtensionService {
public _serviceBrand: any;
private _onDidRegisterExtensions: Emitter<void>;
private readonly _onDidRegisterExtensions: Emitter<void>;
private _registry: ExtensionDescriptionRegistry;
private readonly _installedExtensionsReady: Barrier;
@@ -126,31 +276,18 @@ export class ExtensionService extends Disposable implements IExtensionService {
public readonly onDidChangeExtensionsStatus: Event<string[]> = this._onDidChangeExtensionsStatus.event;
// --- 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 _extensionHostExtensionRuntimeErrors: { [id: string]: Error[]; };
private _extensionHostProcessWorker: ExtensionHostProcessWorker;
private _extensionHostProcessRPCProtocol: RPCProtocol;
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; }>;
private _extensionHostProcessManager: ExtensionHostProcessManager;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@INotificationService private readonly _notificationService: INotificationService,
@IChoiceService private readonly _choiceService: IChoiceService,
@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,
@ILifecycleService lifecycleService: ILifecycleService
@ILifecycleService lifecycleService: ILifecycleService,
@IExtensionManagementService private extensionManagementService: IExtensionManagementService
) {
super();
this._registry = null;
@@ -161,15 +298,12 @@ export class ExtensionService extends Disposable implements IExtensionService {
this._onDidRegisterExtensions = new Emitter<void>();
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
this._extensionHostProcessActivationTimes = Object.create(null);
this._extensionHostExtensionRuntimeErrors = Object.create(null);
this._extensionHostProcessWorker = null;
this._extensionHostProcessRPCProtocol = null;
this._extensionHostProcessCustomers = [];
this._extensionHostProcessProxy = null;
this._extensionHostProcessManager = null;
this.startDelayed(lifecycleService);
if (this._environmentService.disableExtensions) {
this._notificationService.info(nls.localize('extensionsDisabled', "All extensions are disabled."));
}
}
private startDelayed(lifecycleService: ILifecycleService): void {
@@ -223,51 +357,25 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
private _stopExtensionHostProcess(): void {
const previouslyActivatedExtensionIds = Object.keys(this._extensionHostProcessActivationTimes);
let previouslyActivatedExtensionIds: string[] = [];
this._extensionHostProcessFinishedActivateEvents = Object.create(null);
this._extensionHostProcessActivationTimes = Object.create(null);
this._extensionHostExtensionRuntimeErrors = Object.create(null);
if (this._extensionHostProcessWorker) {
this._extensionHostProcessWorker.dispose();
this._extensionHostProcessWorker = null;
if (this._extensionHostProcessManager) {
previouslyActivatedExtensionIds = this._extensionHostProcessManager.getActivatedExtensionIds();
this._extensionHostProcessManager.dispose();
this._extensionHostProcessManager = null;
}
if (this._extensionHostProcessRPCProtocol) {
this._extensionHostProcessRPCProtocol.dispose();
this._extensionHostProcessRPCProtocol = 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;
this._onDidChangeExtensionsStatus.fire(previouslyActivatedExtensionIds);
if (previouslyActivatedExtensionIds.length > 0) {
this._onDidChangeExtensionsStatus.fire(previouslyActivatedExtensionIds);
}
}
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));
});
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, this);
this._extensionHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, initialActivationEvents);
this._extensionHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal));
}
private _onExtensionHostCrashed(code: number, signal: string): void {
@@ -279,49 +387,16 @@ export class ExtensionService extends Disposable implements IExtensionService {
message = nls.localize('extensionHostProcess.unresponsiveCrash', "Extension host terminated because it was not responsive.");
}
this._choiceService.choose(Severity.Error, message, [nls.localize('devTools', "Developer Tools"), nls.localize('restart', "Restart Extension Host")]).then(choice => {
switch (choice) {
case 0 /* Open Dev Tools */:
this._windowService.openDevTools();
break;
case 1 /* Restart Extension Host */:
this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents));
break;
}
});
}
private _createExtensionHostCustomers(protocol: IMessagePassingProtocol): ExtHostExtensionServiceShape {
if (logExtensionHostCommunication || this._environmentService.logExtensionHostCommunication) {
protocol = asLoggingProtocol(protocol);
}
this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol);
const extHostContext: IExtHostContext = this._extensionHostProcessRPCProtocol;
// 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._extensionHostProcessRPCProtocol.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._extensionHostProcessRPCProtocol.assertRegistered(expected);
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
this._notificationService.prompt(Severity.Error, message,
[{
label: nls.localize('devTools', "Open Developer Tools"),
run: () => this._windowService.openDevTools()
},
{
label: nls.localize('restart', "Restart Extension Host"),
run: () => this._startExtensionHostProcess(Object.keys(this._allRequestedActivateEvents))
}]
);
}
// ---- begin IExtensionService
@@ -349,15 +424,11 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
}
protected _activateByEvent(activationEvent: string): TPromise<void> {
if (this._extensionHostProcessFinishedActivateEvents[activationEvent] || !this._extensionHostProcessProxy) {
return NO_OP_VOID_PROMISE;
private _activateByEvent(activationEvent: string): TPromise<void> {
if (this._extensionHostProcessManager) {
return this._extensionHostProcessManager.activateByEvent(activationEvent);
}
return this._extensionHostProcessProxy.then((proxy) => {
return proxy.value.$activateByEvent(activationEvent);
}).then(() => {
this._extensionHostProcessFinishedActivateEvents[activationEvent] = true;
});
return NO_OP_VOID_PROMISE;
}
public whenInstalledExtensionsRegistered(): TPromise<boolean> {
@@ -388,6 +459,9 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
public getExtensionsStatus(): { [id: string]: IExtensionsStatus; } {
const activationTimes = this._extensionHostProcessManager ? this._extensionHostProcessManager.getActivationTimes() : {};
const runtimeErrors = this._extensionHostProcessManager ? this._extensionHostProcessManager.getRuntimeErrors() : {};
let result: { [id: string]: IExtensionsStatus; } = Object.create(null);
if (this._registry) {
const extensions = this._registry.getAllExtensionDescriptions();
@@ -396,8 +470,8 @@ export class ExtensionService extends Disposable implements IExtensionService {
const id = extension.id;
result[id] = {
messages: this._extensionsMessages[id],
activationTimes: this._extensionHostProcessActivationTimes[id],
runtimeErrors: this._extensionHostExtensionRuntimeErrors[id],
activationTimes: activationTimes[id],
runtimeErrors: runtimeErrors[id],
};
}
}
@@ -405,15 +479,15 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
public canProfileExtensionHost(): boolean {
return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort());
if (this._extensionHostProcessManager) {
return this._extensionHostProcessManager.canProfileExtensionHost();
}
return false;
}
public startExtensionHostProfile(): TPromise<ProfileSession> {
if (this._extensionHostProcessWorker) {
let port = this._extensionHostProcessWorker.getInspectPort();
if (port) {
return this._instantiationService.createInstance(ExtensionHostProfiler, port).start();
}
if (this._extensionHostProcessManager) {
return this._extensionHostProcessManager.startExtensionHostProfile();
}
throw new Error('Extension host not running or no inspect port available');
}
@@ -424,7 +498,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
private _scanAndHandleExtensions(): void {
this._getRuntimeExtension()
this._getRuntimeExtensions()
.then(runtimeExtensons => {
this._registry = new ExtensionDescriptionRegistry(runtimeExtensons);
@@ -449,14 +523,13 @@ export class ExtensionService extends Disposable implements IExtensionService {
});
}
private _getRuntimeExtension(): TPromise<IExtensionDescription[]> {
private _getRuntimeExtensions(): TPromise<IExtensionDescription[]> {
const log = new Logger((severity, source, message) => {
this._logOrShowMessage(severity, this._isDev ? messageWithSource2(source, message) : message);
});
return ExtensionService._scanInstalledExtensions(this._windowService, this._choiceService, this._environmentService, log)
return ExtensionService._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, log)
.then(({ system, user, development }) => {
this._extensionEnablementService.migrateToIdentifiers(user); // TODO: @sandy Remove it after couple of milestones
return this._extensionEnablementService.getDisabledExtensions()
.then(disabledExtensions => {
let result: { [extensionId: string]: IExtensionDescription; } = {};
@@ -501,7 +574,11 @@ export class ExtensionService extends Disposable implements IExtensionService {
});
if (extensionsToDisable.length) {
return TPromise.join(extensionsToDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled)))
return this.extensionManagementService.getInstalled(LocalExtensionType.User)
.then(installed => {
const toDisable = installed.filter(i => extensionsToDisable.some(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i) }, e)));
return TPromise.join(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled)));
})
.then(() => {
this._storageService.store(BetterMergeDisabledNowKey, true);
return runtimeExtensions;
@@ -510,7 +587,36 @@ export class ExtensionService extends Disposable implements IExtensionService {
return runtimeExtensions;
}
});
});
}).then(extensions => this._updateEnableProposedApi(extensions));
}
private _updateEnableProposedApi(extensions: IExtensionDescription[]): IExtensionDescription[] {
const enableProposedApiForAll = !this._environmentService.isBuilt || (!!this._environmentService.extensionDevelopmentPath && product.nameLong.indexOf('Insiders') >= 0);
const enableProposedApiFor = this._environmentService.args['enable-proposed-api'] || [];
for (const extension of extensions) {
if (!isFalsyOrEmpty(product.extensionAllowedProposedApi)
&& product.extensionAllowedProposedApi.indexOf(extension.id) >= 0
) {
// fast lane -> proposed api is available to all extensions
// that are listed in product.json-files
extension.enableProposedApi = true;
} else if (extension.enableProposedApi && !extension.isBuiltin) {
if (
!enableProposedApiForAll &&
enableProposedApiFor.indexOf(extension.id) < 0
) {
extension.enableProposedApi = false;
console.error(`Extension '${extension.id} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`);
} else {
// proposed api is available when developing or when an extension was explicitly
// spelled out via a command line argument
console.warn(`Extension '${extension.id}' uses PROPOSED API which is subject to change and removal without notice.`);
}
}
}
return extensions;
}
private _handleExtensionPointMessage(msg: IMessage) {
@@ -531,7 +637,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
const { type, extensionId, extensionPointId, message } = msg;
/* __GDPR__
"extensionsMessage" : {
"type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"extensionId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"extensionPointId": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"message": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
@@ -543,7 +649,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
}
private static async _validateExtensionsCache(windowService: IWindowService, choiceService: IChoiceService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): TPromise<void> {
private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): TPromise<void> {
const cacheFolder = path.join(environmentService.userDataPath, MANIFEST_CACHE_FOLDER);
const cacheFile = path.join(cacheFolder, cacheKey);
@@ -568,11 +674,14 @@ export class ExtensionService extends Disposable implements IExtensionService {
console.error(err);
}
choiceService.choose(Severity.Error, nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."), [nls.localize('reloadWindow', "Reload Window")]).then(choice => {
if (choice === 0) {
windowService.reloadWindow();
}
});
notificationService.prompt(
Severity.Error,
nls.localize('extensionCache.invalid', "Extensions have been modified on disk. Please reload the window."),
[{
label: nls.localize('reloadWindow', "Reload Window"),
run: () => windowService.reloadWindow()
}]
);
}
private static async _readExtensionCache(environmentService: IEnvironmentService, cacheKey: string): TPromise<IExtensionCacheData> {
@@ -606,7 +715,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
}
private static async _scanExtensionsWithCache(windowService: IWindowService, choiceService: IChoiceService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): TPromise<IExtensionDescription[]> {
private static async _scanExtensionsWithCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput, log: ILog): TPromise<IExtensionDescription[]> {
if (input.devMode) {
// Do not cache when running out of sources...
return ExtensionScanner.scanExtensions(input, log);
@@ -624,7 +733,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
// Validate the cache asynchronously after 5s
setTimeout(async () => {
try {
await this._validateExtensionsCache(windowService, choiceService, environmentService, cacheKey, input);
await this._validateExtensionsCache(windowService, notificationService, environmentService, cacheKey, input);
} catch (err) {
errors.onUnexpectedError(err);
}
@@ -646,7 +755,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
return result;
}
private static _scanInstalledExtensions(windowService: IWindowService, choiceService: IChoiceService, environmentService: IEnvironmentService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, log: ILog): TPromise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
const translationConfig: TPromise<Translations> = platform.translationsConfigFile
? pfs.readFile(platform.translationsConfigFile, 'utf8').then((content) => {
@@ -668,7 +777,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
const builtinExtensions = this._scanExtensionsWithCache(
windowService,
choiceService,
notificationService,
environmentService,
BUILTIN_MANIFEST_CACHE_FILE,
new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, translations),
@@ -722,7 +831,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
? TPromise.as([])
: this._scanExtensionsWithCache(
windowService,
choiceService,
notificationService,
environmentService,
USER_MANIFEST_CACHE_FILE,
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, translations),
@@ -798,15 +907,12 @@ export class ExtensionService extends Disposable implements IExtensionService {
}
public _onExtensionActivated(extensionId: string, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
this._extensionHostProcessActivationTimes[extensionId] = new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent);
this._extensionHostProcessManager.onExtensionActivated(extensionId, startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent);
this._onDidChangeExtensionsStatus.fire([extensionId]);
}
public _onExtensionRuntimeError(extensionId: string, err: Error): void {
if (!this._extensionHostExtensionRuntimeErrors[extensionId]) {
this._extensionHostExtensionRuntimeErrors[extensionId] = [];
}
this._extensionHostExtensionRuntimeErrors[extensionId].push(err);
this._extensionHostProcessManager.onExtensionRuntimeError(extensionId, err);
this._onDidChangeExtensionsStatus.fire([extensionId]);
}

View File

@@ -33,6 +33,12 @@ export class ExtensionDescriptionRegistry {
if (Array.isArray(extensionDescription.activationEvents)) {
for (let j = 0, lenJ = extensionDescription.activationEvents.length; j < lenJ; j++) {
let activationEvent = extensionDescription.activationEvents[j];
// TODO@joao: there's no easy way to contribute this
if (activationEvent === 'onUri') {
activationEvent = `onUri:${extensionDescription.id}`;
}
this._activationMap[activationEvent] = this._activationMap[activationEvent] || [];
this._activationMap[activationEvent].push(extensionDescription);
}

View File

@@ -496,7 +496,7 @@ export class ExtensionScanner {
/**
* Read the extension defined in `absoluteFolderPath`
*/
private static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, nlsConfig: NlsConfiguration): TPromise<IExtensionDescription> {
public static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, nlsConfig: NlsConfiguration): TPromise<IExtensionDescription> {
absoluteFolderPath = normalize(absoluteFolderPath);
let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin);
@@ -550,11 +550,13 @@ export class ExtensionScanner {
const gallery: IExtensionReference[] = [];
refs.forEach(ref => {
const { id, version } = getIdAndVersionFromLocalExtensionId(ref.name);
if (!id || !version) {
nonGallery.push(ref);
} else {
gallery.push(ref);
if (ref.name.indexOf('.') !== 0) { // Do not consider user extension folder starting with `.`
const { id, version } = getIdAndVersionFromLocalExtensionId(ref.name);
if (!id || !version) {
nonGallery.push(ref);
} else {
gallery.push(ref);
}
}
});
refs = [...nonGallery, ...gallery];

View File

@@ -10,11 +10,88 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc';
import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise';
import { ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier';
import { CharCode } from 'vs/base/common/charCode';
import URI, { UriComponents } from 'vs/base/common/uri';
import { MarshalledObject } from 'vs/base/common/marshalling';
declare var Proxy: any; // TODO@TypeScript
export interface IURITransformer {
transformIncoming(uri: UriComponents): UriComponents;
transformOutgoing(uri: URI): URI;
}
function _transformOutgoingURIs(obj: any, transformer: IURITransformer, depth: number): any {
if (!obj || depth > 200) {
return null;
}
if (typeof obj === 'object') {
if (obj instanceof URI) {
return transformer.transformOutgoing(obj);
}
// walk object (or array)
for (let key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const r = _transformOutgoingURIs(obj[key], transformer, depth + 1);
if (r !== null) {
obj[key] = r;
}
}
}
}
return null;
}
function transformOutgoingURIs(obj: any, transformer: IURITransformer): any {
const result = _transformOutgoingURIs(obj, transformer, 0);
if (result === null) {
// no change
return obj;
}
return result;
}
function _transformIncomingURIs(obj: any, transformer: IURITransformer, depth: number): any {
if (!obj || depth > 200) {
return null;
}
if (typeof obj === 'object') {
if ((<MarshalledObject>obj).$mid === 1) {
return transformer.transformIncoming(obj);
}
// walk object (or array)
for (let key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const r = _transformIncomingURIs(obj[key], transformer, depth + 1);
if (r !== null) {
obj[key] = r;
}
}
}
}
return null;
}
function transformIncomingURIs(obj: any, transformer: IURITransformer): any {
const result = _transformIncomingURIs(obj, transformer, 0);
if (result === null) {
// no change
return obj;
}
return result;
}
export class RPCProtocol implements IRPCProtocol {
private readonly _uriTransformer: IURITransformer;
private _isDisposed: boolean;
private readonly _locals: { [id: string]: any; };
private readonly _proxies: { [id: string]: any; };
@@ -23,7 +100,8 @@ export class RPCProtocol implements IRPCProtocol {
private readonly _pendingRPCReplies: { [msgId: string]: LazyPromise; };
private readonly _multiplexor: RPCMultiplexer;
constructor(protocol: IMessagePassingProtocol) {
constructor(protocol: IMessagePassingProtocol, transformer: IURITransformer = null) {
this._uriTransformer = transformer;
this._isDisposed = false;
this._locals = Object.create(null);
this._proxies = Object.create(null);
@@ -43,6 +121,13 @@ export class RPCProtocol implements IRPCProtocol {
});
}
public transformIncomingURIs<T>(obj: T): T {
if (!this._uriTransformer) {
return obj;
}
return transformIncomingURIs(obj, this._uriTransformer);
}
public getProxy<T>(identifier: ProxyIdentifier<T>): T {
if (!this._proxies[identifier.id]) {
this._proxies[identifier.id] = this._createProxy(identifier.id);
@@ -84,6 +169,9 @@ export class RPCProtocol implements IRPCProtocol {
}
let msg = <RPCMessage>JSON.parse(rawmsg);
if (this._uriTransformer) {
msg = transformIncomingURIs(msg, this._uriTransformer);
}
switch (msg.type) {
case MessageType.Request:
@@ -109,6 +197,9 @@ export class RPCProtocol implements IRPCProtocol {
this._invokedHandlers[callId].then((r) => {
delete this._invokedHandlers[callId];
if (this._uriTransformer) {
r = transformOutgoingURIs(r, this._uriTransformer);
}
this._multiplexor.send(MessageFactory.replyOK(callId, r));
}, (err) => {
delete this._invokedHandlers[callId];
@@ -185,6 +276,9 @@ export class RPCProtocol implements IRPCProtocol {
});
this._pendingRPCReplies[callId] = result;
if (this._uriTransformer) {
args = transformOutgoingURIs(args, this._uriTransformer);
}
this._multiplexor.send(MessageFactory.request(callId, proxyId, methodName, args));
return result;
}

View File

@@ -0,0 +1,146 @@
/*---------------------------------------------------------------------------------------------
* 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 { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import * as encoding from 'vs/base/node/encoding';
import uri from 'vs/base/common/uri';
import { IResolveContentOptions, isParent, IResourceEncodings } from 'vs/platform/files/common/files';
import { isLinux } from 'vs/base/common/platform';
import { join, extname } from 'path';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
export interface IEncodingOverride {
parent?: uri;
extension?: string;
encoding: string;
}
// TODO@Ben debt - encodings should move one layer up from the file service into the text file
// service and then ideally be passed in as option to the file service
// the file service should talk about string | Buffer for reading and writing and only convert
// to strings if a encoding is provided
export class ResourceEncodings implements IResourceEncodings {
private encodingOverride: IEncodingOverride[];
private toDispose: IDisposable[];
constructor(
private textResourceConfigurationService: ITextResourceConfigurationService,
private environmentService: IEnvironmentService,
private contextService: IWorkspaceContextService,
encodingOverride?: IEncodingOverride[]
) {
this.encodingOverride = encodingOverride || this.getEncodingOverrides();
this.toDispose = [];
this.registerListeners();
}
private registerListeners(): void {
// Workspace Folder Change
this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => {
this.encodingOverride = this.getEncodingOverrides();
}));
}
public getReadEncoding(resource: uri, options: IResolveContentOptions, detected: encoding.IDetectedEncodingResult): string {
let preferredEncoding: string;
// Encoding passed in as option
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
}
}
// Encoding detected
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;
}
}
// Encoding configured
else if (this.textResourceConfigurationService.getValue(resource, 'files.encoding') === encoding.UTF8_with_bom) {
preferredEncoding = encoding.UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then
}
return this.getEncodingForResource(resource, preferredEncoding);
}
public getWriteEncoding(resource: uri, preferredEncoding?: string): string {
return this.getEncodingForResource(resource, preferredEncoding);
}
private getEncodingForResource(resource: uri, preferredEncoding?: string): string {
let fileEncoding: string;
const override = this.getEncodingOverride(resource);
if (override) {
fileEncoding = override; // encoding override always wins
} else if (preferredEncoding) {
fileEncoding = preferredEncoding; // preferred encoding comes second
} else {
fileEncoding = this.textResourceConfigurationService.getValue(resource, 'files.encoding'); // and last we check for settings
}
if (!fileEncoding || !encoding.encodingExists(fileEncoding)) {
fileEncoding = encoding.UTF8; // the default is UTF 8
}
return fileEncoding;
}
private getEncodingOverrides(): IEncodingOverride[] {
const encodingOverride: IEncodingOverride[] = [];
// Global settings
encodingOverride.push({ parent: uri.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 });
// Workspace files
encodingOverride.push({ extension: WORKSPACE_EXTENSION, encoding: encoding.UTF8 });
// Folder Settings
this.contextService.getWorkspace().folders.forEach(folder => {
// {{SQL CARBON EDIT}}
encodingOverride.push({ parent: uri.file(join(folder.uri.fsPath, '.sqlops')), encoding: encoding.UTF8 });
});
return encodingOverride;
}
private getEncodingOverride(resource: uri): string {
if (resource && this.encodingOverride && this.encodingOverride.length) {
for (let i = 0; i < this.encodingOverride.length; i++) {
const override = this.encodingOverride[i];
// check if the resource is child of encoding override path
if (override.parent && isParent(resource.fsPath, override.parent.fsPath, !isLinux /* ignorecase */)) {
return override.encoding;
}
// check if the resource extension is equal to encoding override
if (override.extension && extname(resource.fsPath) === `.${override.extension}`) {
return override.encoding;
}
}
}
return null;
}
public dispose(): void {
this.toDispose = dispose(this.toDispose);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,51 +4,62 @@
*--------------------------------------------------------------------------------------------*/
'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, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult, FileChangesEvent, ICreateFileOptions, FileOperationError, FileOperationResult, ITextSnapshot, snapshotToString } from 'vs/platform/files/common/files';
import { TPromise } from 'vs/base/common/winjs.base';
import { basename, join } from 'path';
import { IDisposable } from 'vs/base/common/lifecycle';
import { isFalsyOrEmpty, distinct } from 'vs/base/common/arrays';
import { posix } from 'path';
import { flatten, isFalsyOrEmpty } from 'vs/base/common/arrays';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { TernarySearchTree, keys } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { Progress } from 'vs/platform/progress/common/progress';
import { decodeStream, encode, UTF8, UTF8_with_bom } from 'vs/base/node/encoding';
import { TernarySearchTree } from 'vs/base/common/map';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IStorageService } from 'vs/platform/storage/common/storage';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { IDecodeStreamOptions, toDecodeStream, encodeStream } from 'vs/base/node/encoding';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { maxBufferLen, detectMimeAndEncodingFromBuffer } from 'vs/base/node/mime';
import { MIME_BINARY } from 'vs/base/common/mime';
import { localize } from 'vs/nls';
import { IChoiceService } from 'vs/platform/dialogs/common/dialogs';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { FileChangesEvent, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileStat, IFileSystemProvider, IFilesConfiguration, IResolveContentOptions, IResolveFileOptions, IResolveFileResult, IStat, IStreamContent, ITextSnapshot, IUpdateContentOptions, StringSnapshot, IWatchOptions, FileType } from 'vs/platform/files/common/files';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { FileService } from 'vs/workbench/services/files/electron-browser/fileService';
import { createReadableOfProvider, createReadableOfSnapshot, createWritableOfProvider } from 'vs/workbench/services/files/electron-browser/streams';
class TypeOnlyStat implements IStat {
constructor(readonly type: FileType) {
//
}
// todo@remote -> make a getter and warn when
// being used in development.
mtime: number = 0;
ctime: number = 0;
size: number = 0;
}
function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): TPromise<IFileStat> {
const [resource, stat] = tuple;
const fileStat: IFileStat = {
isDirectory: false,
isSymbolicLink: stat.type === FileType.Symlink,
resource: resource,
name: basename(resource.path),
resource,
name: posix.basename(resource.path),
isDirectory: (stat.type & FileType.Directory) !== 0,
isSymbolicLink: (stat.type & FileType.SymbolicLink) !== 0,
mtime: stat.mtime,
size: stat.size,
etag: stat.mtime.toString(29) + stat.size.toString(31),
};
if (stat.type === FileType.Dir) {
fileStat.isDirectory = true;
if (fileStat.isDirectory) {
if (recurse && recurse([resource, stat])) {
// dir -> resolve
return provider.readdir(resource).then(entries => {
fileStat.isDirectory = true;
// resolve children if requested
return TPromise.join(entries.map(stat => toIFileStat(provider, stat, recurse))).then(children => {
return TPromise.join(entries.map(tuple => {
const [name, type] = tuple;
const childResource = resource.with({ path: posix.join(resource.path, name) });
return toIFileStat(provider, [childResource, new TypeOnlyStat(type)], recurse);
})).then(children => {
fileStat.children = children;
return fileStat;
});
@@ -74,66 +85,183 @@ export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, ISta
});
}
class WorkspaceWatchLogic {
private _disposables: IDisposable[] = [];
private _watches = new Map<string, URI>();
constructor(
private _fileService: RemoteFileService,
@IConfigurationService private _configurationService: IConfigurationService,
@IWorkspaceContextService private _contextService: IWorkspaceContextService,
) {
this._refresh();
this._disposables.push(this._contextService.onDidChangeWorkspaceFolders(e => {
for (const removed of e.removed) {
this._unwatchWorkspace(removed.uri);
}
for (const added of e.added) {
this._watchWorkspace(added.uri);
}
}));
this._disposables.push(this._contextService.onDidChangeWorkbenchState(e => {
this._refresh();
}));
this._disposables.push(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files.watcherExclude')) {
this._refresh();
}
}));
}
dispose(): void {
this._unwatchWorkspaces();
this._disposables = dispose(this._disposables);
}
private _refresh(): void {
this._unwatchWorkspaces();
for (const folder of this._contextService.getWorkspace().folders) {
if (folder.uri.scheme !== Schemas.file) {
this._watchWorkspace(folder.uri);
}
}
}
private _watchWorkspace(resource: URI) {
let excludes: string[] = [];
let config = this._configurationService.getValue<IFilesConfiguration>({ resource });
if (config.files && config.files.watcherExclude) {
for (const key in config.files.watcherExclude) {
if (config.files.watcherExclude[key] === true) {
excludes.push(key);
}
}
}
this._watches.set(resource.toString(), resource);
this._fileService.watchFileChanges(resource, { recursive: true, excludes });
}
private _unwatchWorkspace(resource: URI) {
if (this._watches.has(resource.toString())) {
this._fileService.unwatchFileChanges(resource);
this._watches.delete(resource.toString());
}
}
private _unwatchWorkspaces() {
this._watches.forEach(uri => this._fileService.unwatchFileChanges(uri));
this._watches.clear();
}
}
export class RemoteFileService extends FileService {
private readonly _provider = new Map<string, IFileSystemProvider>();
private _supportedSchemes: string[];
private readonly _provider: Map<string, IFileSystemProvider>;
private readonly _lastKnownSchemes: string[];
constructor(
@IExtensionService private readonly _extensionService: IExtensionService,
@IStorageService private readonly _storageService: IStorageService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IEnvironmentService environmentService: IEnvironmentService,
@ILifecycleService lifecycleService: ILifecycleService,
@IChoiceService choiceService: IChoiceService,
@INotificationService notificationService: INotificationService,
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
) {
super(
configurationService,
contextService,
environmentService,
lifecycleService,
choiceService,
_storageService,
_environmentService,
textResourceConfigurationService,
configurationService,
lifecycleService,
_storageService,
notificationService
);
this._supportedSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]'));
this._provider = new Map<string, IFileSystemProvider>();
this._lastKnownSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]'));
this.toDispose.push(new WorkspaceWatchLogic(this, configurationService, contextService));
}
registerProvider(authority: string, provider: IFileSystemProvider): IDisposable {
if (this._provider.has(authority)) {
throw new Error();
registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable {
if (this._provider.has(scheme)) {
throw new Error('a provider for that scheme is already registered');
}
this._supportedSchemes.push(authority);
this._storageService.store('remote_schemes', JSON.stringify(distinct(this._supportedSchemes)));
this._provider.set(scheme, provider);
this._onDidChangeFileSystemProviderRegistrations.fire({ added: true, scheme, provider });
this._storageService.store('remote_schemes', JSON.stringify(keys(this._provider)));
this._provider.set(authority, provider);
const reg = provider.onDidChange(changes => {
const reg = provider.onDidChangeFile(changes => {
// forward change events
this._onFileChanges.fire(new FileChangesEvent(changes));
});
return {
dispose: () => {
this._provider.delete(authority);
this._onDidChangeFileSystemProviderRegistrations.fire({ added: false, scheme, provider });
this._provider.delete(scheme);
reg.dispose();
}
};
}
canHandleResource(resource: URI): boolean {
return resource.scheme === Schemas.file
|| this._provider.has(resource.scheme)
// TODO@remote
|| this._supportedSchemes.indexOf(resource.scheme) >= 0;
if (resource.scheme === Schemas.file || this._provider.has(resource.scheme)) {
return true;
}
// TODO@remote
// this needs to go, but this already went viral
// https://github.com/Microsoft/vscode/issues/48275
if (this._lastKnownSchemes.indexOf(resource.scheme) < 0) {
return false;
}
if (!this._environmentService.isBuilt) {
console.warn('[remote] cache information required for ' + resource.toString());
}
return true;
}
private _tryParseFileOperationResult(err: any): FileOperationResult {
if (!(err instanceof Error)) {
return undefined;
}
let match = /^(.+) \(FileSystemError\)$/.exec(err.name);
if (!match) {
return undefined;
}
let res: FileOperationResult;
switch (match[1]) {
case 'EntryNotFound':
res = FileOperationResult.FILE_NOT_FOUND;
break;
case 'EntryIsADirectory':
res = FileOperationResult.FILE_IS_DIRECTORY;
break;
case 'NoPermissions':
res = FileOperationResult.FILE_PERMISSION_DENIED;
break;
case 'EntryExists':
res = FileOperationResult.FILE_MOVE_CONFLICT;
break;
case 'EntryNotADirectory':
default:
// todo
res = undefined;
break;
}
return res;
}
// --- stat
private _withProvider(resource: URI): TPromise<IFileSystemProvider> {
return this._extensionService.activateByEvent('onFileSystemAccess:' + resource.scheme).then(() => {
return TPromise.join([
this._extensionService.activateByEvent('onFileSystem:' + resource.scheme),
this._extensionService.activateByEvent('onFileSystemAccess:' + resource.scheme) // todo@remote -> remove
]).then(() => {
const provider = this._provider.get(resource.scheme);
if (!provider) {
const err = new Error();
@@ -159,7 +287,10 @@ export class RemoteFileService extends FileService {
} else {
return this._doResolveFiles([{ resource, options }]).then(data => {
if (data.length !== 1 || !data[0].success) {
throw new Error(`ENOENT, ${resource}`);
throw new FileOperationError(
localize('fileNotFoundError', "File not found ({0})", resource.toString(true)),
FileOperationResult.FILE_NOT_FOUND
);
} else {
return data[0].stat;
}
@@ -188,9 +319,7 @@ export class RemoteFileService extends FileService {
promises.push(this._doResolveFiles(group));
}
}
return TPromise.join(promises).then(data => {
return [].concat(...data);
});
return TPromise.join(promises).then(data => flatten(data));
}
private _doResolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): TPromise<IResolveFileResult[], any> {
@@ -215,7 +344,7 @@ export class RemoteFileService extends FileService {
if (resource.scheme === Schemas.file) {
return super.resolveContent(resource, options);
} else {
return this._doResolveContent(resource, options).then(RemoteFileService._asContent);
return this._readFile(resource, options).then(RemoteFileService._asContent);
}
}
@@ -223,11 +352,11 @@ export class RemoteFileService extends FileService {
if (resource.scheme === Schemas.file) {
return super.resolveStreamContent(resource, options);
} else {
return this._doResolveContent(resource, options);
return this._readFile(resource, options);
}
}
private _doResolveContent(resource: URI, options: IResolveContentOptions = Object.create(null)): TPromise<IStreamContent> {
private _readFile(resource: URI, options: IResolveContentOptions = Object.create(null)): TPromise<IStreamContent> {
return this._withProvider(resource).then(provider => {
return this.resolveFile(resource).then(fileStat => {
@@ -249,20 +378,18 @@ export class RemoteFileService extends FileService {
);
}
const guessEncoding = options.autoGuessEncoding;
const count = maxBufferLen(options);
const chunks: Buffer[] = [];
const decodeStreamOpts: IDecodeStreamOptions = {
guessEncoding: options.autoGuessEncoding,
overwriteEncoding: detected => {
return this.encoding.getReadEncoding(resource, options, { encoding: detected, seemsBinary: false });
}
};
return provider.read(
resource,
0, count,
new Progress<Buffer>(chunk => chunks.push(chunk))
).then(bytesRead => {
// send to bla
return detectMimeAndEncodingFromBuffer({ bytesRead, buffer: Buffer.concat(chunks) }, guessEncoding);
const readable = createReadableOfProvider(provider, resource, options.position || 0);
}).then(detected => {
if (options.acceptTextOnly && detected.mimes.indexOf(MIME_BINARY) >= 0) {
return toDecodeStream(readable, decodeStreamOpts).then(data => {
if (options.acceptTextOnly && data.detected.seemsBinary) {
return TPromise.wrapError<IStreamContent>(new FileOperationError(
localize('fileBinaryError', "File seems to be binary and cannot be opened as text"),
FileOperationResult.FILE_IS_BINARY,
@@ -270,53 +397,9 @@ export class RemoteFileService extends FileService {
));
}
let preferredEncoding: string;
if (options && options.encoding) {
if (detected.encoding === UTF8 && options.encoding === UTF8) {
preferredEncoding = 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 === UTF8) {
preferredEncoding = UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM
} else {
preferredEncoding = detected.encoding;
}
// todo@remote - encoding logic should not be kept
// hostage inside the node file service
// } else if (super.configuredEncoding(resource) === UTF8_with_bom) {
} else {
preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then
}
// const encoding = this.getEncoding(resource);
const stream = decodeStream(preferredEncoding);
// start with what we have already read
// and have a new stream to read the rest
let offset = 0;
for (const chunk of chunks) {
stream.write(chunk);
offset += chunk.length;
}
if (offset < count) {
// we didn't read enough the first time which means
// that we are done
stream.end();
} else {
// there is more to read
provider.read(resource, offset, -1, new Progress<Buffer>(chunk => stream.write(chunk))).then(() => {
stream.end();
}, err => {
stream.emit('error', err);
stream.end();
});
}
return {
encoding: preferredEncoding,
value: stream,
return <IStreamContent>{
encoding: data.detected.encoding,
value: data.stream,
resource: fileStat.resource,
name: fileStat.name,
etag: fileStat.etag,
@@ -329,25 +412,46 @@ export class RemoteFileService extends FileService {
// --- saving
private static async _mkdirp(provider: IFileSystemProvider, directory: URI): Promise<void> {
let basenames: string[] = [];
while (directory.path !== '/') {
try {
let stat = await provider.stat(directory);
if ((stat.type & FileType.Directory) === 0) {
throw new Error(`${directory.toString()} is not a directory`);
}
break; // we have hit a directory -> good
} catch (e) {
// ENOENT
basenames.push(posix.basename(directory.path));
directory = directory.with({ path: posix.dirname(directory.path) });
}
}
for (let i = basenames.length - 1; i >= 0; i--) {
directory = directory.with({ path: posix.join(directory.path, basenames[i]) });
await provider.mkdir(directory);
}
}
createFile(resource: URI, content?: string, options?: ICreateFileOptions): TPromise<IFileStat> {
if (resource.scheme === Schemas.file) {
return super.createFile(resource, content, options);
} else {
return this._withProvider(resource).then(provider => {
let prepare = options && !options.overwrite
? this.existsFile(resource)
: TPromise.as(false);
return prepare.then(exists => {
if (exists && options && !options.overwrite) {
return TPromise.wrapError(new FileOperationError('EEXIST', FileOperationResult.FILE_MODIFIED_SINCE, options));
}
return this._doUpdateContent(provider, resource, content || '', {});
}).then(fileStat => {
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
return fileStat;
return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => {
const encoding = this.encoding.getWriteEncoding(resource);
return this._writeFile(provider, resource, new StringSnapshot(content), encoding, { create: true, overwrite: Boolean(options && options.overwrite) });
});
}).then(fileStat => {
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
return fileStat;
}, err => {
const message = localize('err.create', "Failed to create file {0}", resource.toString(false));
const result = this._tryParseFileOperationResult(err);
throw new FileOperationError(message, result, options);
});
}
}
@@ -357,15 +461,24 @@ export class RemoteFileService extends FileService {
return super.updateContent(resource, value, options);
} else {
return this._withProvider(resource).then(provider => {
return this._doUpdateContent(provider, resource, value, options || {});
return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => {
const snapshot = typeof value === 'string' ? new StringSnapshot(value) : value;
return this._writeFile(provider, resource, snapshot, options && options.encoding, { create: true, overwrite: true });
});
});
}
}
private _doUpdateContent(provider: IFileSystemProvider, resource: URI, content: string | ITextSnapshot, options: IUpdateContentOptions): TPromise<IFileStat> {
const encoding = this.getEncoding(resource, options.encoding);
// TODO@Joh support streaming API for remote file system writes
return provider.write(resource, encode(typeof content === 'string' ? content : snapshotToString(content), encoding)).then(() => {
private _writeFile(provider: IFileSystemProvider, resource: URI, snapshot: ITextSnapshot, preferredEncoding: string, options: FileWriteOptions): TPromise<IFileStat> {
const readable = createReadableOfSnapshot(snapshot);
const encoding = this.encoding.getWriteEncoding(resource, preferredEncoding);
const encoder = encodeStream(encoding);
const target = createWritableOfProvider(provider, resource, options);
return new TPromise<IFileStat>((resolve, reject) => {
readable.pipe(encoder).pipe(target);
target.once('error', err => reject(err));
target.once('finish', _ => resolve(void 0));
}).then(_ => {
return this.resolveFile(resource);
});
}
@@ -393,9 +506,7 @@ export class RemoteFileService extends FileService {
return super.del(resource, useTrash);
} else {
return this._withProvider(resource).then(provider => {
return provider.stat(resource).then(stat => {
return stat.type === FileType.Dir ? provider.rmdir(resource) : provider.unlink(resource);
}).then(() => {
return provider.delete(resource).then(() => {
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE));
});
});
@@ -407,8 +518,10 @@ export class RemoteFileService extends FileService {
return super.createFolder(resource);
} else {
return this._withProvider(resource).then(provider => {
return provider.mkdir(resource).then(stat => {
return toIFileStat(provider, [resource, stat]);
return RemoteFileService._mkdirp(provider, resource.with({ path: posix.dirname(resource.path) })).then(() => {
return provider.mkdir(resource).then(() => {
return this.resolveFile(resource);
});
});
}).then(fileStat => {
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
@@ -421,7 +534,7 @@ export class RemoteFileService extends FileService {
if (resource.scheme === Schemas.file) {
return super.rename(resource, newName);
} else {
const target = resource.with({ path: join(resource.path, '..', newName) });
const target = resource.with({ path: posix.join(resource.path, '..', newName) });
return this._doMoveWithInScheme(resource, target, false);
}
}
@@ -443,11 +556,17 @@ export class RemoteFileService extends FileService {
: TPromise.as(null);
return prepare.then(() => this._withProvider(source)).then(provider => {
return provider.move(source, target).then(stat => {
return toIFileStat(provider, [target, stat]);
return provider.rename(source, target, { overwrite }).then(() => {
return this.resolveFile(target);
}).then(fileStat => {
this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.MOVE, fileStat));
return fileStat;
}, err => {
const result = this._tryParseFileOperationResult(err);
if (result === FileOperationResult.FILE_MOVE_CONFLICT) {
throw new FileOperationError(localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), result);
}
throw err;
});
});
}
@@ -463,75 +582,99 @@ export class RemoteFileService extends FileService {
});
}
importFile(source: URI, targetFolder: URI): TPromise<IImportResult> {
if (source.scheme === targetFolder.scheme && source.scheme === Schemas.file) {
return super.importFile(source, targetFolder);
} else {
const target = targetFolder.with({ path: join(targetFolder.path, basename(source.path)) });
return this.copyFile(source, target, false).then(stat => ({ stat, isNew: false }));
}
}
copyFile(source: URI, target: URI, overwrite?: boolean): TPromise<IFileStat> {
if (source.scheme === target.scheme && source.scheme === Schemas.file) {
return super.copyFile(source, target, overwrite);
}
const prepare = overwrite
? this.del(target).then(undefined, err => { /*ignore*/ })
: TPromise.as(null);
return this._withProvider(target).then(provider => {
return prepare.then(() => {
// todo@ben, can only copy text files
// https://github.com/Microsoft/vscode/issues/41543
return this.resolveContent(source, { acceptTextOnly: true }).then(content => {
return this._withProvider(target).then(provider => {
return this._doUpdateContent(provider, target, content.value, { encoding: content.encoding }).then(fileStat => {
this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat));
return fileStat;
});
if (source.scheme === target.scheme && (provider.capabilities & FileSystemProviderCapabilities.FileFolderCopy)) {
// good: provider supports copy withing scheme
return provider.copy(source, target, { overwrite }).then(() => {
return this.resolveFile(target);
}).then(fileStat => {
this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat));
return fileStat;
}, err => {
if (err instanceof Error && err.name === 'ENOPRO') {
// file scheme
return super.updateContent(target, content.value, { encoding: content.encoding });
} else {
return TPromise.wrapError(err);
const result = this._tryParseFileOperationResult(err);
if (result === FileOperationResult.FILE_MOVE_CONFLICT) {
throw new FileOperationError(localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), result);
}
throw err;
});
}
const prepare = overwrite
? this.del(target).then(undefined, err => { /*ignore*/ })
: TPromise.as(null);
return prepare.then(() => {
// todo@ben, can only copy text files
// https://github.com/Microsoft/vscode/issues/41543
return this.resolveContent(source, { acceptTextOnly: true }).then(content => {
return this._withProvider(target).then(provider => {
return this._writeFile(
provider, target,
new StringSnapshot(content.value),
content.encoding,
{ create: true, overwrite }
).then(fileStat => {
this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.COPY, fileStat));
return fileStat;
});
}, err => {
const result = this._tryParseFileOperationResult(err);
if (result === FileOperationResult.FILE_MOVE_CONFLICT) {
throw new FileOperationError(localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), result);
} else if (err instanceof Error && err.name === 'ENOPRO') {
// file scheme
return super.updateContent(target, content.value, { encoding: content.encoding });
} else {
return TPromise.wrapError(err);
}
});
});
});
});
}
touchFile(resource: URI): TPromise<IFileStat, any> {
private _activeWatches = new Map<string, { unwatch: Thenable<IDisposable>, count: number }>();
public watchFileChanges(resource: URI, opts?: IWatchOptions): void {
if (resource.scheme === Schemas.file) {
return super.touchFile(resource);
} else {
return this._doTouchFile(resource);
return super.watchFileChanges(resource);
}
}
private _doTouchFile(resource: URI): TPromise<IFileStat> {
return this._withProvider(resource).then(provider => {
return provider.stat(resource).then(() => {
return provider.utimes(resource, Date.now(), Date.now());
if (!opts) {
opts = { recursive: false, excludes: [] };
}
const key = resource.toString();
const entry = this._activeWatches.get(key);
if (entry) {
entry.count += 1;
return;
}
this._activeWatches.set(key, {
count: 1,
unwatch: this._withProvider(resource).then(provider => {
return provider.watch(resource, opts);
}, err => {
return provider.write(resource, new Uint8Array(0));
}).then(() => {
return this.resolveFile(resource);
});
return { dispose() { } };
})
});
}
// TODO@Joh - file watching on demand!
public watchFileChanges(resource: URI): void {
if (resource.scheme === Schemas.file) {
super.watchFileChanges(resource);
}
}
public unwatchFileChanges(resource: URI): void {
if (resource.scheme === Schemas.file) {
super.unwatchFileChanges(resource);
return super.unwatchFileChanges(resource);
}
let entry = this._activeWatches.get(resource.toString());
if (entry && --entry.count === 0) {
entry.unwatch.then(dispose);
this._activeWatches.delete(resource.toString());
}
}
}

View File

@@ -0,0 +1,168 @@
/*---------------------------------------------------------------------------------------------
* 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 { Readable, Writable } from 'stream';
import { UTF8 } from 'vs/base/node/encoding';
import URI from 'vs/base/common/uri';
import { IFileSystemProvider, ITextSnapshot, FileSystemProviderCapabilities, FileWriteOptions } from 'vs/platform/files/common/files';
import { illegalArgument } from 'vs/base/common/errors';
export function createWritableOfProvider(provider: IFileSystemProvider, resource: URI, opts: FileWriteOptions): Writable {
if (provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose) {
return createWritable(provider, resource, opts);
} else if (provider.capabilities & FileSystemProviderCapabilities.FileReadWrite) {
return createSimpleWritable(provider, resource, opts);
} else {
throw illegalArgument();
}
}
function createSimpleWritable(provider: IFileSystemProvider, resource: URI, opts: FileWriteOptions): Writable {
return new class extends Writable {
_chunks: Buffer[] = [];
constructor(opts?) {
super(opts);
}
_write(chunk: Buffer, encoding: string, callback: Function) {
this._chunks.push(chunk);
callback(null);
}
end() {
// todo@joh - end might have another chunk...
provider.writeFile(resource, Buffer.concat(this._chunks), opts).then(_ => {
super.end();
}, err => {
this.emit('error', err);
});
}
};
}
function createWritable(provider: IFileSystemProvider, resource: URI, opts: FileWriteOptions): Writable {
return new class extends Writable {
_fd: number;
_pos: number;
constructor(opts?) {
super(opts);
}
async _write(chunk: Buffer, encoding, callback: Function) {
try {
if (typeof this._fd !== 'number') {
this._fd = await provider.open(resource);
}
let bytesWritten = await provider.write(this._fd, this._pos, chunk, 0, chunk.length);
this._pos += bytesWritten;
callback();
} catch (err) {
callback(err);
}
}
end() {
provider.close(this._fd).then(_ => {
super.end();
}, err => {
this.emit('error', err);
});
}
};
}
export function createReadableOfProvider(provider: IFileSystemProvider, resource: URI, position: number): Readable {
if (provider.capabilities & FileSystemProviderCapabilities.FileOpenReadWriteClose) {
return createReadable(provider, resource, position);
} else if (provider.capabilities & FileSystemProviderCapabilities.FileReadWrite) {
return createSimpleReadable(provider, resource, position);
} else {
throw illegalArgument();
}
}
function createReadable(provider: IFileSystemProvider, resource: URI, position: number): Readable {
return new class extends Readable {
_fd: number;
_pos: number = position;
_reading: boolean = false;
constructor(opts?) {
super(opts);
this.once('close', _ => this._final());
}
async _read(size?: number) {
if (this._reading) {
return;
}
this._reading = true;
try {
if (typeof this._fd !== 'number') {
this._fd = await provider.open(resource);
}
let buffer = Buffer.allocUnsafe(64 * 1024);
while (this._reading) {
let bytesRead = await provider.read(this._fd, this._pos, buffer, 0, buffer.length);
if (bytesRead === 0) {
this._reading = false;
this.push(null);
}
else {
this._reading = this.push(buffer.slice(0, bytesRead));
this._pos += bytesRead;
}
}
}
catch (err) {
//
this.emit('error', err);
}
}
async _final() {
if (typeof this._fd === 'number') {
await provider.close(this._fd);
}
}
};
}
function createSimpleReadable(provider: IFileSystemProvider, resource: URI, position: number): Readable {
return new class extends Readable {
_readOperation: Thenable<any>;
_read(size?: number): void {
if (this._readOperation) {
return;
}
this._readOperation = provider.readFile(resource).then(data => {
this.push(data.slice(position));
this.push(null);
}, err => {
this.emit('error', err);
this.push(null);
});
}
};
}
export function createReadableOfSnapshot(snapshot: ITextSnapshot): Readable {
return new Readable({
read: function () {
try {
let chunk: string;
let canPush = true;
// Push all chunks as long as we can push and as long as
// the underlying snapshot returns strings to us
while (canPush && typeof (chunk = snapshot.read()) === 'string') {
canPush = this.push(chunk);
}
// Signal EOS by pushing NULL
if (typeof chunk !== 'string') {
this.push(null);
}
} catch (error) {
this.emit('error', error);
}
},
encoding: UTF8 // very important, so that strings are passed around and not buffers!
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ 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 * as nsfw from 'vscode-nsfw';
import { IWatcherService, IWatcherRequest } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
import { TPromise, ProgressCallback, TValueCallback, ErrorCallback } from 'vs/base/common/winjs.base';
import { ThrottledDelayer } from 'vs/base/common/async';
@@ -61,7 +61,7 @@ export class NsfwWatcherService implements IWatcherService {
ignored: request.ignored
};
process.on('uncaughtException', e => {
process.on('uncaughtException', (e: Error | string) => {
// Specially handle ENOSPC errors that can happen when
// the watcher consumes so many file descriptors that

View File

@@ -5,8 +5,8 @@
'use strict';
import assert = require('assert');
import platform = require('vs/base/common/platform');
import * as assert from 'assert';
import * as platform from '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';

View File

@@ -5,20 +5,21 @@
'use strict';
import chokidar = require('vscode-chokidar');
import fs = require('fs');
import * as chokidar from 'vscode-chokidar';
import * as fs from 'fs';
import gracefulFs = require('graceful-fs');
import * as gracefulFs from '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 * as strings from '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 * as watcher from 'vs/workbench/services/files/node/watcher/common';
import { IWatcherRequest, IWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcher';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
export class ChokidarWatcherService implements IWatcherService {
@@ -28,6 +29,7 @@ export class ChokidarWatcherService implements IWatcherService {
private spamCheckStartTime: number;
private spamWarningLogged: boolean;
private enospcErrorLogged: boolean;
private toDispose: IDisposable[] = [];
public watch(request: IWatcherRequest): TPromise<void> {
const watcherOpts: chokidar.IOptions = {
@@ -62,6 +64,11 @@ export class ChokidarWatcherService implements IWatcherService {
let undeliveredFileEvents: watcher.IRawFileChange[] = [];
const fileEventDelayer = new ThrottledDelayer(ChokidarWatcherService.FS_EVENT_DELAY);
this.toDispose.push(toDisposable(() => {
chokidarWatcher.close();
fileEventDelayer.cancel();
}));
return new TPromise<void>((c, e, p) => {
chokidarWatcher.on('all', (type: string, path: string) => {
if (isMacintosh) {
@@ -156,8 +163,12 @@ export class ChokidarWatcherService implements IWatcherService {
}
});
}, () => {
chokidarWatcher.close();
fileEventDelayer.cancel();
this.toDispose = dispose(this.toDispose);
});
}
public stop(): TPromise<void> {
this.toDispose = dispose(this.toDispose);
return TPromise.as(void 0);
}
}

View File

@@ -5,11 +5,11 @@
'use strict';
import cp = require('child_process');
import * as cp from '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 * as decoder from 'vs/base/node/decoder';
import * as glob from 'vs/base/common/glob';
import uri from 'vs/base/common/uri';
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';

View File

@@ -5,14 +5,14 @@
'use strict';
import fs = require('fs');
import path = require('path');
import assert = require('assert');
import * as fs from 'fs';
import * as path from 'path';
import * as assert from 'assert';
import { StatResolver } from 'vs/workbench/services/files/node/fileService';
import { StatResolver } from 'vs/workbench/services/files/electron-browser/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');
import * as utils from 'vs/workbench/services/files/test/electron-browser/utils';
function create(relativePath: string): StatResolver {
let basePath = require.toUrl('./fixtures/resolver');
@@ -31,9 +31,9 @@ function toResource(relativePath: string): uri {
suite('Stat Resolver', () => {
test('resolve file', function (done: () => void) {
test('resolve file', function () {
let resolver = create('/index.html');
resolver.resolve(null).then(result => {
return resolver.resolve(null).then(result => {
assert.ok(!result.isDirectory);
assert.equal(result.name, 'index.html');
assert.ok(!!result.etag);
@@ -42,16 +42,15 @@ suite('Stat Resolver', () => {
return resolver.resolve(null).then(result => {
assert.ok(result.isDirectory);
});
})
.done(() => done(), done);
});
});
test('resolve directory', function (done: () => void) {
test('resolve directory', function () {
let testsElements = ['examples', 'other', 'index.html', 'site.css'];
let resolver = create('/');
resolver.resolve(null).then(result => {
return resolver.resolve(null).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children.length > 0);
@@ -78,14 +77,13 @@ suite('Stat Resolver', () => {
assert.ok(!'Unexpected value ' + path.basename(value.resource.fsPath));
}
});
})
.done(() => done(), done);
});
});
test('resolve directory - resolveTo single directory', function (done: () => void) {
test('resolve directory - resolveTo single directory', function () {
let resolver = create('/');
resolver.resolve({ resolveTo: [toResource('other/deep')] }).then(result => {
return resolver.resolve({ resolveTo: [toResource('other/deep')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children.length > 0);
@@ -102,14 +100,13 @@ suite('Stat Resolver', () => {
assert.ok(deep);
assert.ok(deep.children.length > 0);
assert.equal(deep.children.length, 4);
})
.done(() => done(), done);
});
});
test('resolve directory - resolveTo single directory - mixed casing', function (done: () => void) {
test('resolve directory - resolveTo single directory - mixed casing', function () {
let resolver = create('/');
resolver.resolve({ resolveTo: [toResource('other/Deep')] }).then(result => {
return resolver.resolve({ resolveTo: [toResource('other/Deep')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children.length > 0);
@@ -131,14 +128,13 @@ suite('Stat Resolver', () => {
assert.ok(deep.children.length > 0);
assert.equal(deep.children.length, 4);
}
})
.done(() => done(), done);
});
});
test('resolve directory - resolveTo multiple directories', function (done: () => void) {
test('resolve directory - resolveTo multiple directories', function () {
let resolver = create('/');
resolver.resolve({ resolveTo: [toResource('other/deep'), toResource('examples')] }).then(result => {
return resolver.resolve({ resolveTo: [toResource('other/deep'), toResource('examples')] }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children.length > 0);
@@ -160,14 +156,13 @@ suite('Stat Resolver', () => {
assert.ok(examples);
assert.ok(examples.children.length > 0);
assert.equal(examples.children.length, 4);
})
.done(() => done(), done);
});
});
test('resolve directory - resolveSingleChildFolders', function (done: () => void) {
test('resolve directory - resolveSingleChildFolders', function () {
let resolver = create('/other');
resolver.resolve({ resolveSingleChildDescendants: true }).then(result => {
return resolver.resolve({ resolveSingleChildDescendants: true }).then(result => {
assert.ok(result);
assert.ok(result.children);
assert.ok(result.children.length > 0);
@@ -180,7 +175,6 @@ suite('Stat Resolver', () => {
assert.ok(deep);
assert.ok(deep.children.length > 0);
assert.equal(deep.children.length, 4);
})
.done(() => done(), done);
});
});
});

View File

@@ -5,16 +5,16 @@
'use strict';
import assert = require('assert');
import * as assert from 'assert';
import platform = require('vs/base/common/platform');
import * as platform from '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';
import { Event, Emitter } from 'vs/base/common/event';
class TestFileWatcher {
private _onFileChanges: Emitter<FileChangesEvent>;
private readonly _onFileChanges: Emitter<FileChangesEvent>;
constructor() {
this._onFileChanges = new Emitter<FileChangesEvent>();

View File

@@ -8,7 +8,7 @@
import { createDecorator, ServiceIdentifier, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Position, IEditorInput } from 'vs/platform/editor/common/editor';
import { IEditorStacksModel, IEditorGroup, IEditorOpeningEvent } from 'vs/workbench/common/editor';
import Event from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
export enum GroupArrangement {
MINIMIZE_OTHERS,

View File

@@ -6,7 +6,7 @@
'use strict';
import { TPromise } from 'vs/base/common/winjs.base';
import errors = require('vs/base/common/errors');
import * as errors from '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, ITextEditorSelection, Position as GroupPosition } from 'vs/platform/editor/common/editor';

View File

@@ -14,4 +14,5 @@ export const IWorkbenchIssueService = createDecorator<IWorkbenchIssueService>('w
export interface IWorkbenchIssueService {
_serviceBrand: any;
openReporter(dataOverrides?: Partial<IssueReporterData>): TPromise<void>;
openProcessExplorer(): TPromise<void>;
}

View File

@@ -8,10 +8,9 @@
import { IssueReporterStyles, IIssueService, IssueReporterData } from 'vs/platform/issue/common/issue';
import { TPromise } from 'vs/base/common/winjs.base';
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground } from 'vs/platform/theme/common/colorRegistry';
import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground } from 'vs/platform/theme/common/colorRegistry';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IExtensionManagementService, IExtensionEnablementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
import { getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { webFrame } from 'electron';
import { assign } from 'vs/base/common/objects';
import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue';
@@ -29,7 +28,7 @@ export class WorkbenchIssueService implements IWorkbenchIssueService {
openReporter(dataOverrides: Partial<IssueReporterData> = {}): TPromise<void> {
return this.extensionManagementService.getInstalled(LocalExtensionType.User).then(extensions => {
const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled({ id: getGalleryExtensionIdFromLocal(extension) }));
const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled(extension));
const theme = this.themeService.getTheme();
const issueReporterData: IssueReporterData = assign(
{
@@ -42,6 +41,21 @@ export class WorkbenchIssueService implements IWorkbenchIssueService {
return this.issueService.openReporter(issueReporterData);
});
}
openProcessExplorer(): TPromise<void> {
const theme = this.themeService.getTheme();
const data = {
zoomLevel: webFrame.getZoomLevel(),
styles: {
backgroundColor: theme.getColor(editorBackground) && theme.getColor(editorBackground).toString(),
color: theme.getColor(editorForeground).toString(),
hoverBackground: theme.getColor(listHoverBackground) && theme.getColor(listHoverBackground).toString(),
hoverForeground: theme.getColor(listHoverForeground) && theme.getColor(listHoverForeground).toString(),
highlightForeground: theme.getColor(listHighlightForeground) && theme.getColor(listHighlightForeground).toString()
}
};
return this.issueService.openProcessExplorer(data);
}
}
export function getIssueReporterStyles(theme: ITheme): IssueReporterStyles {

View File

@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import nls = require('vs/nls');
import * as nls from 'vs/nls';
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import URI from 'vs/base/common/uri';
import strings = require('vs/base/common/strings');
import paths = require('vs/base/common/paths');
import * as strings from 'vs/base/common/strings';
import * as paths from 'vs/base/common/paths';
interface IJSONValidationExtensionPoint {
fileMatch: string;

View File

@@ -74,6 +74,9 @@ export class NativeResolvedKeybinding extends ResolvedKeybinding {
constructor(mapper: MacLinuxKeyboardMapper, OS: OperatingSystem, firstPart: ScanCodeBinding, chordPart: ScanCodeBinding) {
super();
if (!firstPart) {
throw new Error(`Invalid USLayoutResolvedKeybinding`);
}
this._mapper = mapper;
this._OS = OS;
this._firstPart = firstPart;

View File

@@ -86,6 +86,9 @@ export class WindowsNativeResolvedKeybinding extends ResolvedKeybinding {
constructor(mapper: WindowsKeyboardMapper, firstPart: SimpleKeybinding, chordPart: SimpleKeybinding) {
super();
if (!firstPart) {
throw new Error(`Invalid WindowsNativeResolvedKeybinding firstPart`);
}
this._mapper = mapper;
this._firstPart = firstPart;
this._chordPart = chordPart;

View File

@@ -31,7 +31,7 @@ import { IKeyboardMapper, CachedKeyboardMapper } from 'vs/workbench/services/key
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 { 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';
@@ -46,8 +46,8 @@ export class KeyboardMapperFactory {
private _keyboardMapper: IKeyboardMapper;
private _initialized: boolean;
private _onDidChangeKeyboardMapper: Emitter<void> = new Emitter<void>();
public onDidChangeKeyboardMapper: Event<void> = this._onDidChangeKeyboardMapper.event;
private readonly _onDidChangeKeyboardMapper: Emitter<void> = new Emitter<void>();
public readonly onDidChangeKeyboardMapper: Event<void> = this._onDidChangeKeyboardMapper.event;
private constructor() {
this._layoutInfo = null;
@@ -415,7 +415,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
/* __GDPR__
"customKeybindingsChanged" : {
"keyCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"keyCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this._telemetryService.publicLog('customKeybindingsChanged', {
@@ -567,10 +567,10 @@ let schema: IJSONSchema = {
}
};
let schemaRegistry = <IJSONContributionRegistry>Registry.as(Extensions.JSONContribution);
let schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
schemaRegistry.registerSchema(schemaId, schema);
const configurationRegistry = <IConfigurationRegistry>Registry.as(ConfigExtensions.Configuration);
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigExtensions.Configuration);
const keyboardConfiguration: IConfigurationNode = {
'id': 'keyboard',
'order': 15,

View File

@@ -5,22 +5,23 @@
'use strict';
import assert = require('assert');
import os = require('os');
import path = require('path');
import fs = require('fs');
import * as assert from 'assert';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import * as json from 'vs/base/common/json';
import { OS } from 'vs/base/common/platform';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { TPromise } from 'vs/base/common/winjs.base';
import { KeyCode, SimpleKeybinding, ChordKeybinding } from 'vs/base/common/keyCodes';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import extfs = require('vs/base/node/extfs');
import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices';
import * as extfs from 'vs/base/node/extfs';
import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { IWorkspaceContextService, Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace';
import uuid = require('vs/base/common/uuid');
import * as uuid from 'vs/base/common/uuid';
import { ConfigurationService } from 'vs/platform/configuration/node/configurationService';
import { FileService } from 'vs/workbench/services/files/node/fileService';
import { FileService } from 'vs/workbench/services/files/electron-browser/fileService';
import { IFileService } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
@@ -80,7 +81,16 @@ suite('Keybindings Editing', () => {
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModeService, ModeServiceImpl);
instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl));
instantiationService.stub(IFileService, new FileService(new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), lifecycleService, { disableWatcher: true }));
instantiationService.stub(IFileService, new FileService(
new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))),
TestEnvironmentService,
new TestTextResourceConfigurationService(),
new TestConfigurationService(),
lifecycleService,
new TestStorageService(),
new TestNotificationService(),
{ disableWatcher: true })
);
instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
@@ -109,28 +119,28 @@ suite('Keybindings Editing', () => {
fs.writeFileSync(keybindingsFile, ',,,,,,,,,,,,,,');
return testObject.editKeybinding('alt+c', aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }))
.then(() => assert.fail('Should fail with parse errors'),
error => assert.equal(error.message, 'Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.'));
error => assert.equal(error.message, 'Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.'));
});
test('errors cases - parse errors 2', () => {
fs.writeFileSync(keybindingsFile, '[{"key": }]');
return testObject.editKeybinding('alt+c', aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }))
.then(() => assert.fail('Should fail with parse errors'),
error => assert.equal(error.message, 'Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.'));
error => assert.equal(error.message, 'Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.'));
});
test('errors cases - dirty', () => {
instantiationService.stub(ITextFileService, 'isDirty', true);
return testObject.editKeybinding('alt+c', aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }))
.then(() => assert.fail('Should fail with dirty error'),
error => assert.equal(error.message, 'Unable to write because the keybindings configuration file is dirty. Please save it first and then try again.'));
error => assert.equal(error.message, 'Unable to write because the keybindings configuration file is dirty. Please save it first and then try again.'));
});
test('errors cases - did not find an array', () => {
fs.writeFileSync(keybindingsFile, '{"key": "alt+c", "command": "hello"}');
return testObject.editKeybinding('alt+c', aResolvedKeybindingItem({ firstPart: { keyCode: KeyCode.Escape } }))
.then(() => assert.fail('Should fail with dirty error'),
error => assert.equal(error.message, 'Unable to write to the keybindings configuration file. It has an object which is not of type Array. Please open the file to clean up and try again.'));
error => assert.equal(error.message, 'Unable to write to the keybindings configuration file. It has an object which is not of type Array. Please open the file to clean up and try again.'));
});
test('edit a default keybinding to an empty file', () => {

View File

@@ -62,20 +62,16 @@ export function readRawMapping<T>(file: string): TPromise<T> {
});
}
export function assertMapping(writeFileIfDifferent: boolean, mapper: IKeyboardMapper, file: string, done: (err?: any) => void): void {
export function assertMapping(writeFileIfDifferent: boolean, mapper: IKeyboardMapper, file: string): TPromise<void> {
const filePath = require.toUrl(`vs/workbench/services/keybinding/test/${file}`);
readFile(filePath).then((buff) => {
return readFile(filePath).then((buff) => {
let expected = buff.toString();
const actual = mapper.dumpDebugInfo();
if (actual !== expected && writeFileIfDifferent) {
writeFile(filePath, actual);
}
try {
assert.deepEqual(actual.split(/\r\n|\n/), expected.split(/\r\n|\n/));
} catch (err) {
return done(err);
}
done();
}, done);
assert.deepEqual(actual.split(/\r\n|\n/), expected.split(/\r\n|\n/));
});
}

View File

@@ -27,15 +27,14 @@ suite('keyboardMapper - MAC de_ch', () => {
let mapper: MacLinuxKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(false, 'mac_de_ch', OperatingSystem.Macintosh).then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(false, 'mac_de_ch', OperatingSystem.Macintosh).then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'mac_de_ch.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'mac_de_ch.txt');
});
function assertKeybindingTranslation(kb: number, expected: string | string[]): void {
@@ -378,15 +377,14 @@ suite('keyboardMapper - MAC en_us', () => {
let mapper: MacLinuxKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(true, 'mac_en_us', OperatingSystem.Macintosh).then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(true, 'mac_en_us', OperatingSystem.Macintosh).then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'mac_en_us.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'mac_en_us.txt');
});
test('resolveUserBinding Cmd+[Comma] Cmd+/', () => {
@@ -457,15 +455,14 @@ suite('keyboardMapper - LINUX de_ch', () => {
let mapper: MacLinuxKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(false, 'linux_de_ch', OperatingSystem.Linux).then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(false, 'linux_de_ch', OperatingSystem.Linux).then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'linux_de_ch.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'linux_de_ch.txt');
});
function assertKeybindingTranslation(kb: number, expected: string | string[]): void {
@@ -808,15 +805,14 @@ suite('keyboardMapper - LINUX en_us', () => {
let mapper: MacLinuxKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(true, 'linux_en_us', OperatingSystem.Linux).then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(true, 'linux_en_us', OperatingSystem.Linux).then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'linux_en_us.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'linux_en_us.txt');
});
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
@@ -1328,15 +1324,14 @@ suite('keyboardMapper - LINUX ru', () => {
let mapper: MacLinuxKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(false, 'linux_ru', OperatingSystem.Linux).then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(false, 'linux_ru', OperatingSystem.Linux).then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'linux_ru.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'linux_ru.txt');
});
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
@@ -1363,15 +1358,14 @@ suite('keyboardMapper - LINUX en_uk', () => {
let mapper: MacLinuxKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(false, 'linux_en_uk', OperatingSystem.Linux).then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(false, 'linux_en_uk', OperatingSystem.Linux).then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'linux_en_uk.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'linux_en_uk.txt');
});
test('issue #24522: resolveKeyboardEvent Ctrl+Alt+[Minus]', () => {
@@ -1402,15 +1396,14 @@ suite('keyboardMapper - MAC zh_hant', () => {
let mapper: MacLinuxKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(false, 'mac_zh_hant', OperatingSystem.Macintosh).then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(false, 'mac_zh_hant', OperatingSystem.Macintosh).then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'mac_zh_hant.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'mac_zh_hant.txt');
});
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {

View File

@@ -28,15 +28,14 @@ suite('keyboardMapper - WINDOWS de_ch', () => {
let mapper: WindowsKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(false, 'win_de_ch').then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(false, 'win_de_ch').then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'win_de_ch.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'win_de_ch.txt');
});
test('resolveKeybinding Ctrl+A', () => {
@@ -321,15 +320,14 @@ suite('keyboardMapper - WINDOWS en_us', () => {
let mapper: WindowsKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(true, 'win_en_us').then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(true, 'win_en_us').then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'win_en_us.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'win_en_us.txt');
});
test('resolveKeybinding Ctrl+K Ctrl+\\', () => {
@@ -410,15 +408,14 @@ suite('keyboardMapper - WINDOWS por_ptb', () => {
let mapper: WindowsKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(false, 'win_por_ptb').then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(false, 'win_por_ptb').then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'win_por_ptb.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'win_por_ptb.txt');
});
test('resolveKeyboardEvent Ctrl+[IntlRo]', () => {
@@ -472,15 +469,14 @@ suite('keyboardMapper - WINDOWS ru', () => {
let mapper: WindowsKeyboardMapper;
suiteSetup((done) => {
createKeyboardMapper(false, 'win_ru').then((_mapper) => {
suiteSetup(() => {
return createKeyboardMapper(false, 'win_ru').then((_mapper) => {
mapper = _mapper;
done();
}, done);
});
});
test('mapping', (done) => {
assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'win_ru.txt', done);
test('mapping', () => {
return assertMapping(WRITE_FILE_IF_DIFFERENT, mapper, 'win_ru.txt');
});
test('issue ##24361: resolveKeybinding Ctrl+K Ctrl+K', () => {

View File

@@ -8,7 +8,7 @@ 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 * as mime from 'vs/base/common/mime';
import { IFilesConfiguration, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionPointUser, ExtensionMessageCollector, IExtensionPoint, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';

View File

@@ -3,6 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice } from 'vs/platform/notification/common/notification';
import { INotificationsModel, NotificationsModel } from 'vs/workbench/common/notifications';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import Event from 'vs/base/common/event';
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';

View File

@@ -6,7 +6,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
import Event from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
export enum Parts {
ACTIVITYBAR_PART,
@@ -28,7 +28,7 @@ export interface ILayoutOptions {
source?: Parts;
}
export interface Dimension {
export interface IDimension {
readonly width: number;
readonly height: number;
}
@@ -46,7 +46,7 @@ export interface IPartService {
/**
* Emits when the editor part's layout changes.
*/
onEditorLayout: Event<Dimension>;
onEditorLayout: Event<IDimension>;
/**
* Asks the part service to layout all parts.
@@ -135,12 +135,12 @@ export interface IPartService {
isEditorLayoutCentered(): boolean;
/**
* Toggles the workbench in and out of centered editor layout.
* Sets the workbench in and out of centered editor layout.
*/
toggleCenteredEditorLayout(): void;
centerEditorLayout(active: boolean): void;
/**
* Resizes currently focused part on main access
*/
resizePart(part: Parts, sizeChange: number): void;
}
}

View File

@@ -0,0 +1,494 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as network from 'vs/base/common/network';
import { TPromise } from 'vs/base/common/winjs.base';
import * as nls from 'vs/nls';
import URI from 'vs/base/common/uri';
import * as labels from 'vs/base/common/labels';
import * as strings from 'vs/base/common/strings';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { EditorInput } from 'vs/workbench/common/editor';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
import { Position as EditorPosition, IEditor, IEditorOptions } from 'vs/platform/editor/common/editor';
import { ITextModel } from 'vs/editor/common/model';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IPreferencesService, IPreferencesEditorModel, ISetting, getSettingsTargetName, FOLDER_SETTINGS_PATH, DEFAULT_SETTINGS_EDITOR_SETTING } from 'vs/workbench/services/preferences/common/preferences';
import { SettingsEditorModel, DefaultSettingsEditorModel, DefaultKeybindingsEditorModel, defaultKeybindingsContents, DefaultSettings, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { DefaultPreferencesEditorInput, PreferencesEditorInput, KeybindingsEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { getCodeEditor } from 'vs/editor/browser/services/codeEditorService';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { Position, IPosition } from 'vs/editor/common/core/position';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IModeService } from 'vs/editor/common/services/modeService';
import { parse } from 'vs/base/common/json';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { INotificationService } from 'vs/platform/notification/common/notification';
const emptyEditableSettingsContent = '{\n}';
export class PreferencesService extends Disposable implements IPreferencesService {
_serviceBrand: any;
private lastOpenedSettingsInput: PreferencesEditorInput = null;
private readonly _onDispose: Emitter<void> = new Emitter<void>();
private _defaultUserSettingsUriCounter = 0;
private _defaultUserSettingsContentModel: DefaultSettings;
private _defaultWorkspaceSettingsUriCounter = 0;
private _defaultWorkspaceSettingsContentModel: DefaultSettings;
private _defaultFolderSettingsUriCounter = 0;
private _defaultFolderSettingsContentModel: DefaultSettings;
constructor(
@IWorkbenchEditorService private editorService: IWorkbenchEditorService,
@IEditorGroupService private editorGroupService: IEditorGroupService,
@IFileService private fileService: IFileService,
@IWorkspaceConfigurationService private configurationService: IWorkspaceConfigurationService,
@INotificationService private notificationService: INotificationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IInstantiationService private instantiationService: IInstantiationService,
@IEnvironmentService private environmentService: IEnvironmentService,
@ITelemetryService private telemetryService: ITelemetryService,
@ITextModelService private textModelResolverService: ITextModelService,
@IKeybindingService keybindingService: IKeybindingService,
@IModelService private modelService: IModelService,
@IJSONEditingService private jsonEditingService: IJSONEditingService,
@IModeService private modeService: IModeService
) {
super();
// The default keybindings.json updates based on keyboard layouts, so here we make sure
// if a model has been given out we update it accordingly.
keybindingService.onDidUpdateKeybindings(() => {
const model = modelService.getModel(this.defaultKeybindingsResource);
if (!model) {
// model has not been given out => nothing to do
return;
}
modelService.updateModel(model, defaultKeybindingsContents(keybindingService));
});
}
readonly defaultKeybindingsResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/keybindings.json' });
private readonly defaultSettingsRawResource = URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: '/defaultSettings.json' });
get userSettingsResource(): URI {
return this.getEditableSettingsURI(ConfigurationTarget.USER);
}
get workspaceSettingsResource(): URI {
return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE);
}
getFolderSettingsResource(resource: URI): URI {
return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, resource);
}
resolveModel(uri: URI): TPromise<ITextModel> {
if (this.isDefaultSettingsResource(uri)) {
const target = this.getConfigurationTargetFromDefaultSettingsResource(uri);
const mode = this.modeService.getOrCreateMode('jsonc');
const model = this._register(this.modelService.createModel('', mode, uri));
let defaultSettings: DefaultSettings;
this.configurationService.onDidChangeConfiguration(e => {
if (e.source === ConfigurationTarget.DEFAULT) {
const model = this.modelService.getModel(uri);
if (!model) {
// model has not been given out => nothing to do
return;
}
defaultSettings = this.getDefaultSettings(target);
this.modelService.updateModel(model, defaultSettings.parse());
defaultSettings._onDidChange.fire();
}
});
// Check if Default settings is already created and updated in above promise
if (!defaultSettings) {
defaultSettings = this.getDefaultSettings(target);
this.modelService.updateModel(model, defaultSettings.parse());
}
return TPromise.as(model);
}
if (this.defaultSettingsRawResource.toString() === uri.toString()) {
let defaultSettings: DefaultSettings = this.getDefaultSettings(ConfigurationTarget.USER);
const mode = this.modeService.getOrCreateMode('jsonc');
const model = this._register(this.modelService.createModel(defaultSettings.raw, mode, uri));
return TPromise.as(model);
}
if (this.defaultKeybindingsResource.toString() === uri.toString()) {
const defaultKeybindingsEditorModel = this.instantiationService.createInstance(DefaultKeybindingsEditorModel, uri);
const mode = this.modeService.getOrCreateMode('jsonc');
const model = this._register(this.modelService.createModel(defaultKeybindingsEditorModel.content, mode, uri));
return TPromise.as(model);
}
return TPromise.as(null);
}
createPreferencesEditorModel(uri: URI): TPromise<IPreferencesEditorModel<any>> {
if (this.isDefaultSettingsResource(uri)) {
return this.createDefaultSettingsEditorModel(uri);
}
if (this.getEditableSettingsURI(ConfigurationTarget.USER).toString() === uri.toString()) {
return this.createEditableSettingsEditorModel(ConfigurationTarget.USER, uri);
}
const workspaceSettingsUri = this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE);
if (workspaceSettingsUri && workspaceSettingsUri.toString() === uri.toString()) {
return this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE, workspaceSettingsUri);
}
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
return this.createEditableSettingsEditorModel(ConfigurationTarget.WORKSPACE_FOLDER, uri);
}
return TPromise.wrap<IPreferencesEditorModel<any>>(null);
}
openRawDefaultSettings(): TPromise<void> {
return this.editorService.openEditor({ resource: this.defaultSettingsRawResource }, EditorPosition.ONE) as TPromise<any>;
}
openSettings(): TPromise<IEditor> {
const editorInput = this.getActiveSettingsEditorInput() || this.lastOpenedSettingsInput;
const resource = editorInput ? editorInput.master.getResource() : this.userSettingsResource;
const target = this.getConfigurationTargetFromSettingsResource(resource);
return this.openOrSwitchSettings(target, resource);
}
openGlobalSettings(options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
return this.openOrSwitchSettings(ConfigurationTarget.USER, this.userSettingsResource, options, position);
}
openWorkspaceSettings(options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
this.notificationService.info(nls.localize('openFolderFirst', "Open a folder first to create workspace settings"));
return TPromise.as(null);
}
return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE, this.workspaceSettingsResource, options, position);
}
openFolderSettings(folder: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
return this.openOrSwitchSettings(ConfigurationTarget.WORKSPACE_FOLDER, this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, folder), options, position);
}
switchSettings(target: ConfigurationTarget, resource: URI): TPromise<void> {
const activeEditor = this.editorService.getActiveEditor();
if (activeEditor && activeEditor.input instanceof PreferencesEditorInput) {
return this.doSwitchSettings(target, resource, activeEditor.input, activeEditor.position).then(() => null);
} else {
return this.doOpenSettings(target, resource).then(() => null);
}
}
openGlobalKeybindingSettings(textual: boolean): TPromise<void> {
/* __GDPR__
"openKeybindings" : {
"textual" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
}
*/
this.telemetryService.publicLog('openKeybindings', { textual });
if (textual) {
const emptyContents = '// ' + nls.localize('emptyKeybindingsHeader', "Place your key bindings in this file to overwrite the defaults") + '\n[\n]';
const editableKeybindings = URI.file(this.environmentService.appKeybindingsPath);
// Create as needed and open in editor
return this.createIfNotExists(editableKeybindings, emptyContents).then(() => {
return this.editorService.openEditors([
{ input: { resource: this.defaultKeybindingsResource, options: { pinned: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }, position: EditorPosition.ONE },
{ input: { resource: editableKeybindings, options: { pinned: true } }, position: EditorPosition.TWO },
]).then(() => {
this.editorGroupService.focusGroup(EditorPosition.TWO);
});
});
}
return this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { pinned: true }).then(() => null);
}
configureSettingsForLanguage(language: string): void {
this.openGlobalSettings()
.then(editor => {
const codeEditor = getCodeEditor(editor);
this.getPosition(language, codeEditor)
.then(position => {
codeEditor.setPosition(position);
codeEditor.focus();
});
});
}
private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
const activeGroup = this.editorGroupService.getStacksModel().activeGroup;
const positionToReplace = position !== void 0 ? position : activeGroup ? this.editorGroupService.getStacksModel().positionOfGroup(activeGroup) : EditorPosition.ONE;
const editorInput = this.getActiveSettingsEditorInput(positionToReplace);
if (editorInput && editorInput.master.getResource().fsPath !== resource.fsPath) {
return this.doSwitchSettings(configurationTarget, resource, editorInput, positionToReplace);
}
return this.doOpenSettings(configurationTarget, resource, options, position);
}
private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: IEditorOptions, position?: EditorPosition): TPromise<IEditor> {
const openDefaultSettings = !!this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING);
return this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource)
.then(editableSettingsEditorInput => {
if (!options) {
options = { pinned: true };
} else {
options.pinned = true;
}
if (openDefaultSettings) {
const defaultPreferencesEditorInput = this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(configurationTarget));
const preferencesEditorInput = new PreferencesEditorInput(this.getPreferencesEditorInputName(configurationTarget, resource), editableSettingsEditorInput.getDescription(), defaultPreferencesEditorInput, <EditorInput>editableSettingsEditorInput);
this.lastOpenedSettingsInput = preferencesEditorInput;
return this.editorService.openEditor(preferencesEditorInput, options, position);
}
return this.editorService.openEditor(editableSettingsEditorInput, options, position);
});
}
private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, position?: EditorPosition): TPromise<IEditor> {
return this.getOrCreateEditableSettingsEditorInput(target, this.getEditableSettingsURI(target, resource))
.then(toInput => {
const replaceWith = new PreferencesEditorInput(this.getPreferencesEditorInputName(target, resource), toInput.getDescription(), this.instantiationService.createInstance(DefaultPreferencesEditorInput, this.getDefaultSettingsResource(target)), toInput);
return this.editorService.replaceEditors([{
toReplace: input,
replaceWith
}], position).then(editors => {
this.lastOpenedSettingsInput = replaceWith;
return editors[0];
});
});
}
private getActiveSettingsEditorInput(position?: EditorPosition): PreferencesEditorInput {
const stacksModel = this.editorGroupService.getStacksModel();
const group = position !== void 0 ? stacksModel.groupAt(position) : stacksModel.activeGroup;
return group && <PreferencesEditorInput>group.getEditors().filter(e => e instanceof PreferencesEditorInput)[0];
}
private getConfigurationTargetFromSettingsResource(resource: URI): ConfigurationTarget {
if (this.userSettingsResource.toString() === resource.toString()) {
return ConfigurationTarget.USER;
}
const workspaceSettingsResource = this.workspaceSettingsResource;
if (workspaceSettingsResource && workspaceSettingsResource.toString() === resource.toString()) {
return ConfigurationTarget.WORKSPACE;
}
const folder = this.contextService.getWorkspaceFolder(resource);
if (folder) {
return ConfigurationTarget.WORKSPACE_FOLDER;
}
return ConfigurationTarget.USER;
}
private getConfigurationTargetFromDefaultSettingsResource(uri: URI) {
return this.isDefaultWorkspaceSettingsResource(uri) ? ConfigurationTarget.WORKSPACE : this.isDefaultFolderSettingsResource(uri) ? ConfigurationTarget.WORKSPACE_FOLDER : ConfigurationTarget.USER;
}
private isDefaultSettingsResource(uri: URI): boolean {
return this.isDefaultUserSettingsResource(uri) || this.isDefaultWorkspaceSettingsResource(uri) || this.isDefaultFolderSettingsResource(uri);
}
private isDefaultUserSettingsResource(uri: URI): boolean {
return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?settings\.json$/);
}
private isDefaultWorkspaceSettingsResource(uri: URI): boolean {
return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?workspaceSettings\.json$/);
}
private isDefaultFolderSettingsResource(uri: URI): boolean {
return uri.authority === 'defaultsettings' && uri.scheme === network.Schemas.vscode && !!uri.path.match(/\/(\d+\/)?resourceSettings\.json$/);
}
private getDefaultSettingsResource(configurationTarget: ConfigurationTarget): URI {
switch (configurationTarget) {
case ConfigurationTarget.WORKSPACE:
return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultWorkspaceSettingsUriCounter++}/workspaceSettings.json` });
case ConfigurationTarget.WORKSPACE_FOLDER:
return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultFolderSettingsUriCounter++}/resourceSettings.json` });
}
return URI.from({ scheme: network.Schemas.vscode, authority: 'defaultsettings', path: `/${this._defaultUserSettingsUriCounter++}/settings.json` });
}
private getPreferencesEditorInputName(target: ConfigurationTarget, resource: URI): string {
const name = getSettingsTargetName(target, resource, this.contextService);
return target === ConfigurationTarget.WORKSPACE_FOLDER ? nls.localize('folderSettingsName', "{0} (Folder Settings)", name) : name;
}
private getOrCreateEditableSettingsEditorInput(target: ConfigurationTarget, resource: URI): TPromise<EditorInput> {
return this.createSettingsIfNotExists(target, resource)
.then(() => <EditorInput>this.editorService.createInput({ resource }));
}
private createEditableSettingsEditorModel(configurationTarget: ConfigurationTarget, resource: URI): TPromise<SettingsEditorModel> {
const settingsUri = this.getEditableSettingsURI(configurationTarget, resource);
if (settingsUri) {
const workspace = this.contextService.getWorkspace();
if (workspace.configuration && workspace.configuration.toString() === settingsUri.toString()) {
return this.textModelResolverService.createModelReference(settingsUri)
.then(reference => this.instantiationService.createInstance(WorkspaceConfigurationEditorModel, reference, configurationTarget));
}
return this.textModelResolverService.createModelReference(settingsUri)
.then(reference => this.instantiationService.createInstance(SettingsEditorModel, reference, configurationTarget));
}
return TPromise.wrap<SettingsEditorModel>(null);
}
private createDefaultSettingsEditorModel(defaultSettingsUri: URI): TPromise<DefaultSettingsEditorModel> {
return this.textModelResolverService.createModelReference(defaultSettingsUri)
.then(reference => {
const target = this.getConfigurationTargetFromDefaultSettingsResource(defaultSettingsUri);
return this.instantiationService.createInstance(DefaultSettingsEditorModel, defaultSettingsUri, reference, this.getDefaultSettings(target));
});
}
private getDefaultSettings(target: ConfigurationTarget): DefaultSettings {
if (target === ConfigurationTarget.WORKSPACE) {
if (!this._defaultWorkspaceSettingsContentModel) {
this._defaultWorkspaceSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target);
}
return this._defaultWorkspaceSettingsContentModel;
}
if (target === ConfigurationTarget.WORKSPACE_FOLDER) {
if (!this._defaultFolderSettingsContentModel) {
this._defaultFolderSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target);
}
return this._defaultFolderSettingsContentModel;
}
if (!this._defaultUserSettingsContentModel) {
this._defaultUserSettingsContentModel = new DefaultSettings(this.getMostCommonlyUsedSettings(), target);
}
return this._defaultUserSettingsContentModel;
}
private getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): URI {
switch (configurationTarget) {
case ConfigurationTarget.USER:
return URI.file(this.environmentService.appSettingsPath);
case ConfigurationTarget.WORKSPACE:
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
return null;
}
const workspace = this.contextService.getWorkspace();
return workspace.configuration || workspace.folders[0].toResource(FOLDER_SETTINGS_PATH);
case ConfigurationTarget.WORKSPACE_FOLDER:
const folder = this.contextService.getWorkspaceFolder(resource);
return folder ? folder.toResource(FOLDER_SETTINGS_PATH) : null;
}
return null;
}
private createSettingsIfNotExists(target: ConfigurationTarget, resource: URI): TPromise<void> {
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && target === ConfigurationTarget.WORKSPACE) {
return this.fileService.resolveContent(this.contextService.getWorkspace().configuration)
.then(content => {
if (Object.keys(parse(content.value)).indexOf('settings') === -1) {
return this.jsonEditingService.write(resource, { key: 'settings', value: {} }, true).then(null, () => { });
}
return null;
});
}
return this.createIfNotExists(resource, emptyEditableSettingsContent).then(() => { });
}
private createIfNotExists(resource: URI, contents: string): TPromise<any> {
return this.fileService.resolveContent(resource, { acceptTextOnly: true }).then(null, error => {
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
return this.fileService.updateContent(resource, contents).then(null, error => {
return TPromise.wrapError(new Error(nls.localize('fail.createSettings', "Unable to create '{0}' ({1}).", labels.getPathLabel(resource, this.contextService, this.environmentService), error)));
});
}
return TPromise.wrapError(error);
});
}
private getMostCommonlyUsedSettings(): string[] {
return [
'files.autoSave',
'editor.fontSize',
'editor.fontFamily',
'editor.tabSize',
'editor.renderWhitespace',
'editor.cursorStyle',
'editor.multiCursorModifier',
'editor.insertSpaces',
'editor.wordWrap',
'files.exclude',
'files.associations'
];
}
private getPosition(language: string, codeEditor: ICodeEditor): TPromise<IPosition> {
return this.createPreferencesEditorModel(this.userSettingsResource)
.then((settingsModel: IPreferencesEditorModel<ISetting>) => {
const languageKey = `[${language}]`;
let setting = settingsModel.getPreference(languageKey);
const model = codeEditor.getModel();
const configuration = this.configurationService.getValue<{ editor: { tabSize: number; insertSpaces: boolean }, files: { eol: string } }>();
const eol = configuration.files && configuration.files.eol;
if (setting) {
if (setting.overrides.length) {
const lastSetting = setting.overrides[setting.overrides.length - 1];
let content;
if (lastSetting.valueRange.endLineNumber === setting.range.endLineNumber) {
content = ',' + eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor);
} else {
content = ',' + eol + this.spaces(2, configuration.editor);
}
const editOperation = EditOperation.insert(new Position(lastSetting.valueRange.endLineNumber, lastSetting.valueRange.endColumn), content);
model.pushEditOperations([], [editOperation], () => []);
return { lineNumber: lastSetting.valueRange.endLineNumber + 1, column: model.getLineMaxColumn(lastSetting.valueRange.endLineNumber + 1) };
}
return { lineNumber: setting.valueRange.startLineNumber, column: setting.valueRange.startColumn + 1 };
}
return this.configurationService.updateValue(languageKey, {}, ConfigurationTarget.USER)
.then(() => {
setting = settingsModel.getPreference(languageKey);
let content = eol + this.spaces(2, configuration.editor) + eol + this.spaces(1, configuration.editor);
let editOperation = EditOperation.insert(new Position(setting.valueRange.endLineNumber, setting.valueRange.endColumn - 1), content);
model.pushEditOperations([], [editOperation], () => []);
let lineNumber = setting.valueRange.endLineNumber + 1;
settingsModel.dispose();
return { lineNumber, column: model.getLineMaxColumn(lineNumber) };
});
});
}
private spaces(count: number, { tabSize, insertSpaces }: { tabSize: number; insertSpaces: boolean }): string {
return insertSpaces ? strings.repeat(' ', tabSize * count) : strings.repeat('\t', count);
}
public dispose(): void {
this._onDispose.fire();
super.dispose();
}
}

View File

@@ -0,0 +1,571 @@
/*---------------------------------------------------------------------------------------------
* 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 { TPromise } from 'vs/base/common/winjs.base';
import { distinct } from 'vs/base/common/arrays';
import * as strings from 'vs/base/common/strings';
import { OperatingSystem, language, LANGUAGE_DEFAULT } from 'vs/base/common/platform';
import { IMatch, IFilter, or, matchesContiguousSubString, matchesPrefix, matchesCamelCase, matchesWords } from 'vs/base/common/filters';
import { Registry } from 'vs/platform/registry/common/platform';
import { ResolvedKeybinding, ResolvedKeybindingPart } from 'vs/base/common/keyCodes';
import { AriaLabelProvider, UserSettingsLabelProvider, UILabelProvider, ModifierLabels as ModLabels } from 'vs/base/common/keybindingLabels';
import { MenuRegistry, ILocalizedString, ICommandAction } from 'vs/platform/actions/common/actions';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { EditorModel } from 'vs/workbench/common/editor';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
export const KEYBINDING_ENTRY_TEMPLATE_ID = 'keybinding.entry.template';
export const KEYBINDING_HEADER_TEMPLATE_ID = 'keybinding.header.template';
const SOURCE_DEFAULT = localize('default', "Default");
const SOURCE_USER = localize('user', "User");
export interface KeybindingMatch {
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
keyCode?: boolean;
}
export interface KeybindingMatches {
firstPart: KeybindingMatch;
chordPart: KeybindingMatch;
}
export interface IListEntry {
id: string;
templateId: string;
}
export interface IKeybindingItemEntry extends IListEntry {
keybindingItem: IKeybindingItem;
commandIdMatches?: IMatch[];
commandLabelMatches?: IMatch[];
commandDefaultLabelMatches?: IMatch[];
sourceMatches?: IMatch[];
whenMatches?: IMatch[];
keybindingMatches?: KeybindingMatches;
}
export interface IKeybindingItem {
keybinding: ResolvedKeybinding;
keybindingItem: ResolvedKeybindingItem;
commandLabel: string;
commandDefaultLabel: string;
command: string;
source: string;
when: string;
}
interface ModifierLabels {
ui: ModLabels;
aria: ModLabels;
user: ModLabels;
}
const wordFilter = or(matchesPrefix, matchesWords, matchesContiguousSubString);
export class KeybindingsEditorModel extends EditorModel {
private _keybindingItems: IKeybindingItem[];
private _keybindingItemsSortedByPrecedence: IKeybindingItem[];
private modifierLabels: ModifierLabels;
constructor(
os: OperatingSystem,
@IKeybindingService private keybindingsService: IKeybindingService,
@IExtensionService private extensionService: IExtensionService
) {
super();
this.modifierLabels = {
ui: UILabelProvider.modifierLabels[os],
aria: AriaLabelProvider.modifierLabels[os],
user: UserSettingsLabelProvider.modifierLabels[os]
};
}
public fetch(searchValue: string, sortByPrecedence: boolean = false): IKeybindingItemEntry[] {
let keybindingItems = sortByPrecedence ? this._keybindingItemsSortedByPrecedence : this._keybindingItems;
if (/@source:\s*(user|default)/i.test(searchValue)) {
keybindingItems = this.filterBySource(keybindingItems, searchValue);
searchValue = searchValue.replace(/@source:\s*(user|default)/i, '');
}
searchValue = searchValue.trim();
if (!searchValue) {
return keybindingItems.map(keybindingItem => ({ id: KeybindingsEditorModel.getId(keybindingItem), keybindingItem, templateId: KEYBINDING_ENTRY_TEMPLATE_ID }));
}
return this.filterByText(keybindingItems, searchValue);
}
private filterBySource(keybindingItems: IKeybindingItem[], searchValue: string): IKeybindingItem[] {
if (/@source:\s*default/i.test(searchValue)) {
return keybindingItems.filter(k => k.source === SOURCE_DEFAULT);
}
if (/@source:\s*user/i.test(searchValue)) {
return keybindingItems.filter(k => k.source === SOURCE_USER);
}
return keybindingItems;
}
private filterByText(keybindingItems: IKeybindingItem[], searchValue: string): IKeybindingItemEntry[] {
const quoteAtFirstChar = searchValue.charAt(0) === '"';
const quoteAtLastChar = searchValue.charAt(searchValue.length - 1) === '"';
const completeMatch = quoteAtFirstChar && quoteAtLastChar;
if (quoteAtFirstChar) {
searchValue = searchValue.substring(1);
}
if (quoteAtLastChar) {
searchValue = searchValue.substring(0, searchValue.length - 1);
}
searchValue = searchValue.trim();
const result: IKeybindingItemEntry[] = [];
const words = searchValue.split(' ');
const keybindingWords = this.splitKeybindingWords(words);
for (const keybindingItem of keybindingItems) {
let keybindingMatches = new KeybindingItemMatches(this.modifierLabels, keybindingItem, searchValue, words, keybindingWords, completeMatch);
if (keybindingMatches.commandIdMatches
|| keybindingMatches.commandLabelMatches
|| keybindingMatches.commandDefaultLabelMatches
|| keybindingMatches.sourceMatches
|| keybindingMatches.whenMatches
|| keybindingMatches.keybindingMatches) {
result.push({
id: KeybindingsEditorModel.getId(keybindingItem),
templateId: KEYBINDING_ENTRY_TEMPLATE_ID,
commandLabelMatches: keybindingMatches.commandLabelMatches,
commandDefaultLabelMatches: keybindingMatches.commandDefaultLabelMatches,
keybindingItem,
keybindingMatches: keybindingMatches.keybindingMatches,
commandIdMatches: keybindingMatches.commandIdMatches,
sourceMatches: keybindingMatches.sourceMatches,
whenMatches: keybindingMatches.whenMatches
});
}
}
return result;
}
private splitKeybindingWords(wordsSeparatedBySpaces: string[]): string[] {
const result = [];
for (const word of wordsSeparatedBySpaces) {
result.push(...word.split('+').filter(w => !!w));
}
return result;
}
public resolve(editorActionsLabels: { [id: string]: string; }): TPromise<EditorModel> {
return this.extensionService.whenInstalledExtensionsRegistered()
.then(() => {
const workbenchActionsRegistry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
this._keybindingItemsSortedByPrecedence = [];
const boundCommands: Map<string, boolean> = new Map<string, boolean>();
for (const keybinding of this.keybindingsService.getKeybindings()) {
if (keybinding.command) { // Skip keybindings without commands
this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(keybinding.command, keybinding, workbenchActionsRegistry, editorActionsLabels));
boundCommands.set(keybinding.command, true);
}
}
const commandsWithDefaultKeybindings = this.keybindingsService.getDefaultKeybindings().map(keybinding => keybinding.command);
for (const command of KeybindingResolver.getAllUnboundCommands(boundCommands)) {
const keybindingItem = new ResolvedKeybindingItem(null, command, null, null, commandsWithDefaultKeybindings.indexOf(command) === -1);
this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(command, keybindingItem, workbenchActionsRegistry, editorActionsLabels));
}
this._keybindingItems = this._keybindingItemsSortedByPrecedence.slice(0).sort((a, b) => KeybindingsEditorModel.compareKeybindingData(a, b));
return this;
});
}
private static getId(keybindingItem: IKeybindingItem): string {
return keybindingItem.command + (keybindingItem.keybinding ? keybindingItem.keybinding.getAriaLabel() : '') + keybindingItem.source + keybindingItem.when;
}
private static compareKeybindingData(a: IKeybindingItem, b: IKeybindingItem): number {
if (a.keybinding && !b.keybinding) {
return -1;
}
if (b.keybinding && !a.keybinding) {
return 1;
}
if (a.commandLabel && !b.commandLabel) {
return -1;
}
if (b.commandLabel && !a.commandLabel) {
return 1;
}
if (a.commandLabel && b.commandLabel) {
if (a.commandLabel !== b.commandLabel) {
return a.commandLabel.localeCompare(b.commandLabel);
}
}
if (a.command === b.command) {
return a.keybindingItem.isDefault ? 1 : -1;
}
return a.command.localeCompare(b.command);
}
private static toKeybindingEntry(command: string, keybindingItem: ResolvedKeybindingItem, workbenchActionsRegistry: IWorkbenchActionRegistry, editorActions: { [id: string]: string; }): IKeybindingItem {
const menuCommand = MenuRegistry.getCommand(command);
const editorActionLabel = editorActions[command];
return <IKeybindingItem>{
keybinding: keybindingItem.resolvedKeybinding,
keybindingItem,
command,
commandLabel: KeybindingsEditorModel.getCommandLabel(menuCommand, editorActionLabel),
commandDefaultLabel: KeybindingsEditorModel.getCommandDefaultLabel(menuCommand, workbenchActionsRegistry),
when: keybindingItem.when ? keybindingItem.when.serialize() : '',
source: keybindingItem.isDefault ? SOURCE_DEFAULT : SOURCE_USER
};
}
private static getCommandDefaultLabel(menuCommand: ICommandAction, workbenchActionsRegistry: IWorkbenchActionRegistry): string {
if (language !== LANGUAGE_DEFAULT) {
if (menuCommand && menuCommand.title && (<ILocalizedString>menuCommand.title).original) {
return (<ILocalizedString>menuCommand.title).original;
}
}
return null;
}
private static getCommandLabel(menuCommand: ICommandAction, editorActionLabel: string): string {
if (menuCommand) {
return typeof menuCommand.title === 'string' ? menuCommand.title : menuCommand.title.value;
}
if (editorActionLabel) {
return editorActionLabel;
}
return '';
}
}
class KeybindingItemMatches {
public readonly commandIdMatches: IMatch[] = null;
public readonly commandLabelMatches: IMatch[] = null;
public readonly commandDefaultLabelMatches: IMatch[] = null;
public readonly sourceMatches: IMatch[] = null;
public readonly whenMatches: IMatch[] = null;
public readonly keybindingMatches: KeybindingMatches = null;
constructor(private modifierLabels: ModifierLabels, keybindingItem: IKeybindingItem, searchValue: string, words: string[], keybindingWords: string[], private completeMatch: boolean) {
this.commandIdMatches = this.matches(searchValue, keybindingItem.command, or(matchesWords, matchesCamelCase), words);
this.commandLabelMatches = keybindingItem.commandLabel ? this.matches(searchValue, keybindingItem.commandLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandLabel, true), words) : null;
this.commandDefaultLabelMatches = keybindingItem.commandDefaultLabel ? this.matches(searchValue, keybindingItem.commandDefaultLabel, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.commandDefaultLabel, true), words) : null;
this.sourceMatches = this.matches(searchValue, keybindingItem.source, (word, wordToMatchAgainst) => matchesWords(word, keybindingItem.source, true), words);
this.whenMatches = keybindingItem.when ? this.matches(searchValue, keybindingItem.when, or(matchesWords, matchesCamelCase), words) : null;
this.keybindingMatches = keybindingItem.keybinding ? this.matchesKeybinding(keybindingItem.keybinding, searchValue, keybindingWords) : null;
}
private matches(searchValue: string, wordToMatchAgainst: string, wordMatchesFilter: IFilter, words: string[]): IMatch[] {
let matches = wordFilter(searchValue, wordToMatchAgainst);
if (!matches) {
matches = this.matchesWords(words, wordToMatchAgainst, wordMatchesFilter);
}
if (matches) {
matches = this.filterAndSort(matches);
}
return matches;
}
private matchesWords(words: string[], wordToMatchAgainst: string, wordMatchesFilter: IFilter): IMatch[] {
let matches: IMatch[] = [];
for (const word of words) {
const wordMatches = wordMatchesFilter(word, wordToMatchAgainst);
if (wordMatches) {
matches = [...(matches || []), ...wordMatches];
} else {
matches = null;
break;
}
}
return matches;
}
private filterAndSort(matches: IMatch[]): IMatch[] {
return distinct(matches, (a => a.start + '.' + a.end)).filter(match => !matches.some(m => !(m.start === match.start && m.end === match.end) && (m.start <= match.start && m.end >= match.end))).sort((a, b) => a.start - b.start);
}
private matchesKeybinding(keybinding: ResolvedKeybinding, searchValue: string, words: string[]): KeybindingMatches {
const [firstPart, chordPart] = keybinding.getParts();
if (strings.compareIgnoreCase(searchValue, keybinding.getAriaLabel()) === 0 || strings.compareIgnoreCase(searchValue, keybinding.getLabel()) === 0) {
return {
firstPart: this.createCompleteMatch(firstPart),
chordPart: this.createCompleteMatch(chordPart)
};
}
let firstPartMatch: KeybindingMatch = {};
let chordPartMatch: KeybindingMatch = {};
const matchedWords = [];
let firstPartMatchedWords = [];
let chordPartMatchedWords = [];
let matchFirstPart = true;
for (let index = 0; index < words.length; index++) {
const word = words[index];
let firstPartMatched = false;
let chordPartMatched = false;
matchFirstPart = matchFirstPart && !firstPartMatch.keyCode;
let matchChordPart = !chordPartMatch.keyCode;
if (matchFirstPart) {
firstPartMatched = this.matchPart(firstPart, firstPartMatch, word);
if (firstPartMatch.keyCode) {
for (const cordPartMatchedWordIndex of chordPartMatchedWords) {
if (firstPartMatchedWords.indexOf(cordPartMatchedWordIndex) === -1) {
matchedWords.splice(matchedWords.indexOf(cordPartMatchedWordIndex), 1);
}
}
chordPartMatch = {};
chordPartMatchedWords = [];
matchChordPart = false;
}
}
if (matchChordPart) {
chordPartMatched = this.matchPart(chordPart, chordPartMatch, word);
}
if (firstPartMatched) {
firstPartMatchedWords.push(index);
}
if (chordPartMatched) {
chordPartMatchedWords.push(index);
}
if (firstPartMatched || chordPartMatched) {
matchedWords.push(index);
}
matchFirstPart = matchFirstPart && this.isModifier(word);
}
if (matchedWords.length !== words.length) {
return null;
}
if (this.completeMatch && (!this.isCompleteMatch(firstPart, firstPartMatch) || !this.isCompleteMatch(chordPart, chordPartMatch))) {
return null;
}
return this.hasAnyMatch(firstPartMatch) || this.hasAnyMatch(chordPartMatch) ? { firstPart: firstPartMatch, chordPart: chordPartMatch } : null;
}
private matchPart(part: ResolvedKeybindingPart, match: KeybindingMatch, word: string): boolean {
let matched = false;
if (this.matchesMetaModifier(part, word)) {
matched = true;
match.metaKey = true;
}
if (this.matchesCtrlModifier(part, word)) {
matched = true;
match.ctrlKey = true;
}
if (this.matchesShiftModifier(part, word)) {
matched = true;
match.shiftKey = true;
}
if (this.matchesAltModifier(part, word)) {
matched = true;
match.altKey = true;
}
if (this.matchesKeyCode(part, word)) {
match.keyCode = true;
matched = true;
}
return matched;
}
private matchesKeyCode(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
const ariaLabel = keybinding.keyAriaLabel;
if (this.completeMatch || ariaLabel.length === 1 || word.length === 1) {
if (strings.compareIgnoreCase(ariaLabel, word) === 0) {
return true;
}
} else {
if (matchesContiguousSubString(word, ariaLabel)) {
return true;
}
}
return false;
}
private matchesMetaModifier(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
if (!keybinding.metaKey) {
return false;
}
return this.wordMatchesMetaModifier(word);
}
private wordMatchesMetaModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.metaKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.metaKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.metaKey, word)) {
return true;
}
if (matchesPrefix(localize('meta', "meta"), word)) {
return true;
}
return false;
}
private matchesCtrlModifier(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
if (!keybinding.ctrlKey) {
return false;
}
return this.wordMatchesCtrlModifier(word);
}
private wordMatchesCtrlModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.ctrlKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.ctrlKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.ctrlKey, word)) {
return true;
}
return false;
}
private matchesShiftModifier(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
if (!keybinding.shiftKey) {
return false;
}
return this.wordMatchesShiftModifier(word);
}
private wordMatchesShiftModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.shiftKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.shiftKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.shiftKey, word)) {
return true;
}
return false;
}
private matchesAltModifier(keybinding: ResolvedKeybindingPart, word: string): boolean {
if (!keybinding) {
return false;
}
if (!keybinding.altKey) {
return false;
}
return this.wordMatchesAltModifier(word);
}
private wordMatchesAltModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.altKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.altKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.altKey, word)) {
return true;
}
if (matchesPrefix(localize('option', "option"), word)) {
return true;
}
return false;
}
private hasAnyMatch(keybindingMatch: KeybindingMatch): boolean {
return keybindingMatch.altKey ||
keybindingMatch.ctrlKey ||
keybindingMatch.metaKey ||
keybindingMatch.shiftKey ||
keybindingMatch.keyCode;
}
private isCompleteMatch(part: ResolvedKeybindingPart, match: KeybindingMatch): boolean {
if (!part) {
return true;
}
if (!match.keyCode) {
return false;
}
if (part.metaKey && !match.metaKey) {
return false;
}
if (part.altKey && !match.altKey) {
return false;
}
if (part.ctrlKey && !match.ctrlKey) {
return false;
}
if (part.shiftKey && !match.shiftKey) {
return false;
}
return true;
}
private createCompleteMatch(part: ResolvedKeybindingPart): KeybindingMatch {
let match: KeybindingMatch = {};
if (part) {
match.keyCode = true;
if (part.metaKey) {
match.metaKey = true;
}
if (part.altKey) {
match.altKey = true;
}
if (part.ctrlKey) {
match.ctrlKey = true;
}
if (part.shiftKey) {
match.shiftKey = true;
}
}
return match;
}
private isModifier(word: string): boolean {
if (this.wordMatchesAltModifier(word)) {
return true;
}
if (this.wordMatchesCtrlModifier(word)) {
return true;
}
if (this.wordMatchesMetaModifier(word)) {
return true;
}
if (this.wordMatchesShiftModifier(word)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,164 @@
/*---------------------------------------------------------------------------------------------
* 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 } from 'vs/platform/instantiation/common/instantiation';
import { IEditor, Position, IEditorOptions } from 'vs/platform/editor/common/editor';
import { ITextModel } from 'vs/editor/common/model';
import { IRange } from 'vs/editor/common/core/range';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { join } from 'vs/base/common/paths';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { Event } from 'vs/base/common/event';
import { IStringDictionary } from 'vs/base/common/collections';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { localize } from 'vs/nls';
export interface ISettingsGroup {
id: string;
range: IRange;
title: string;
titleRange: IRange;
sections: ISettingsSection[];
}
export interface ISettingsSection {
titleRange?: IRange;
title?: string;
settings: ISetting[];
}
export interface ISetting {
range: IRange;
key: string;
keyRange: IRange;
value: any;
valueRange: IRange;
description: string[];
descriptionRanges: IRange[];
overrides?: ISetting[];
overrideOf?: ISetting;
}
export interface IExtensionSetting extends ISetting {
extensionName: string;
extensionPublisher: string;
}
export interface ISearchResult {
filterMatches: ISettingMatch[];
metadata?: IFilterMetadata;
}
export interface ISearchResultGroup {
id: string;
label: string;
result: ISearchResult;
order: number;
}
export interface IFilterResult {
query?: string;
filteredGroups: ISettingsGroup[];
allGroups: ISettingsGroup[];
matches: IRange[];
metadata?: IStringDictionary<IFilterMetadata>;
}
export interface ISettingMatch {
setting: ISetting;
matches: IRange[];
score: number;
}
export interface IScoredResults {
[key: string]: IRemoteSetting;
}
export interface IRemoteSetting {
score: number;
key: string;
id: string;
defaultValue: string;
description: string;
packageId: string;
extensionName?: string;
extensionPublisher?: string;
}
export interface IFilterMetadata {
requestUrl: string;
requestBody: string;
timestamp: number;
duration: number;
scoredResults: IScoredResults;
extensions?: ILocalExtension[];
/** The number of requests made, since requests are split by number of filters */
requestCount?: number;
/** The name of the server that actually served the request */
context: string;
}
export interface IPreferencesEditorModel<T> {
uri: URI;
getPreference(key: string): T;
dispose(): void;
}
export type IGroupFilter = (group: ISettingsGroup) => boolean;
export type ISettingMatcher = (setting: ISetting, group: ISettingsGroup) => { matches: IRange[], score: number };
export interface ISettingsEditorModel extends IPreferencesEditorModel<ISetting> {
readonly onDidChangeGroups: Event<void>;
settingsGroups: ISettingsGroup[];
filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[];
findValueMatches(filter: string, setting: ISetting): IRange[];
updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult;
}
export interface IKeybindingsEditorModel<T> extends IPreferencesEditorModel<T> {
}
export const IPreferencesService = createDecorator<IPreferencesService>('preferencesService');
export interface IPreferencesService {
_serviceBrand: any;
userSettingsResource: URI;
workspaceSettingsResource: URI;
getFolderSettingsResource(resource: URI): URI;
resolveModel(uri: URI): TPromise<ITextModel>;
createPreferencesEditorModel<T>(uri: URI): TPromise<IPreferencesEditorModel<T>>;
openRawDefaultSettings(): TPromise<void>;
openSettings(): TPromise<IEditor>;
openGlobalSettings(options?: IEditorOptions, position?: Position): TPromise<IEditor>;
openWorkspaceSettings(options?: IEditorOptions, position?: Position): TPromise<IEditor>;
openFolderSettings(folder: URI, options?: IEditorOptions, position?: Position): TPromise<IEditor>;
switchSettings(target: ConfigurationTarget, resource: URI): TPromise<void>;
openGlobalKeybindingSettings(textual: boolean): TPromise<void>;
configureSettingsForLanguage(language: string): void;
}
export function getSettingsTargetName(target: ConfigurationTarget, resource: URI, workspaceContextService: IWorkspaceContextService): string {
switch (target) {
case ConfigurationTarget.USER:
return localize('userSettingsTarget', "User Settings");
case ConfigurationTarget.WORKSPACE:
return localize('workspaceSettingsTarget', "Workspace Settings");
case ConfigurationTarget.WORKSPACE_FOLDER:
const folder = workspaceContextService.getWorkspaceFolder(resource);
return folder ? folder.name : '';
}
return '';
}
export const FOLDER_SETTINGS_PATH = join('.vscode', 'settings.json');
export const DEFAULT_SETTINGS_EDITOR_SETTING = 'workbench.settings.openDefaultSettings';

View File

@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* 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 { SideBySideEditorInput, EditorInput } from 'vs/workbench/common/editor';
import { Verbosity } from 'vs/platform/editor/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import URI from 'vs/base/common/uri';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { IHashService } from 'vs/workbench/services/hash/common/hashService';
import { KeybindingsEditorModel } from 'vs/workbench/services/preferences/common/keybindingsEditorModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { OS } from 'vs/base/common/platform';
import { TPromise } from 'vs/base/common/winjs.base';
export class PreferencesEditorInput extends SideBySideEditorInput {
public static readonly ID: string = 'workbench.editorinputs.preferencesEditorInput';
getTypeId(): string {
return PreferencesEditorInput.ID;
}
public supportsSplitEditor(): boolean {
return true;
}
public getTitle(verbosity: Verbosity): string {
return this.master.getTitle(verbosity);
}
}
export class DefaultPreferencesEditorInput extends ResourceEditorInput {
public static readonly ID = 'workbench.editorinputs.defaultpreferences';
constructor(defaultSettingsResource: URI,
@ITextModelService textModelResolverService: ITextModelService,
@IHashService hashService: IHashService
) {
super(nls.localize('settingsEditorName', "Default Settings"), '', defaultSettingsResource, textModelResolverService, hashService);
}
getTypeId(): string {
return DefaultPreferencesEditorInput.ID;
}
matches(other: any): boolean {
if (other instanceof DefaultPreferencesEditorInput) {
return true;
}
if (!super.matches(other)) {
return false;
}
return true;
}
}
export class KeybindingsEditorInput extends EditorInput {
public static readonly ID: string = 'workbench.input.keybindings';
public readonly keybindingsModel: KeybindingsEditorModel;
constructor(@IInstantiationService instantiationService: IInstantiationService) {
super();
this.keybindingsModel = instantiationService.createInstance(KeybindingsEditorModel, OS);
}
getTypeId(): string {
return KeybindingsEditorInput.ID;
}
getName(): string {
return nls.localize('keybindingsInputName', "Keyboard Shortcuts");
}
resolve(refresh?: boolean): TPromise<KeybindingsEditorModel> {
return TPromise.as(this.keybindingsModel);
}
matches(otherInput: any): boolean {
return otherInput instanceof KeybindingsEditorInput;
}
}

View File

@@ -0,0 +1,926 @@
/*---------------------------------------------------------------------------------------------
* 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 { assign } from 'vs/base/common/objects';
import * as map from 'vs/base/common/map';
import { tail, flatten } from 'vs/base/common/arrays';
import URI from 'vs/base/common/uri';
import { IReference, Disposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { Registry } from 'vs/platform/registry/common/platform';
import { visit, JSONVisitor } from 'vs/base/common/json';
import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
import { EditorModel } from 'vs/workbench/common/editor';
import { IConfigurationNode, IConfigurationRegistry, Extensions, OVERRIDE_PROPERTY_PATTERN, IConfigurationPropertySchema, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import { ISettingsEditorModel, IKeybindingsEditorModel, ISettingsGroup, ISetting, IFilterResult, IGroupFilter, ISettingMatcher, ISettingMatch, ISearchResultGroup, IFilterMetadata } from 'vs/workbench/services/preferences/common/preferences';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { Selection } from 'vs/editor/common/core/selection';
import { IStringDictionary } from 'vs/base/common/collections';
export abstract class AbstractSettingsModel extends EditorModel {
protected _currentResultGroups = new Map<string, ISearchResultGroup>();
public updateResultGroup(id: string, resultGroup: ISearchResultGroup): IFilterResult {
if (resultGroup) {
this._currentResultGroups.set(id, resultGroup);
} else {
this._currentResultGroups.delete(id);
}
this.removeDuplicateResults();
return this.update();
}
/**
* Remove duplicates between result groups, preferring results in earlier groups
*/
private removeDuplicateResults(): void {
const settingKeys = new Set<string>();
map.keys(this._currentResultGroups)
.sort((a, b) => this._currentResultGroups.get(a).order - this._currentResultGroups.get(b).order)
.forEach(groupId => {
const group = this._currentResultGroups.get(groupId);
group.result.filterMatches = group.result.filterMatches.filter(s => !settingKeys.has(s.setting.key));
group.result.filterMatches.forEach(s => settingKeys.add(s.setting.key));
});
}
public filterSettings(filter: string, groupFilter: IGroupFilter, settingMatcher: ISettingMatcher): ISettingMatch[] {
const allGroups = this.filterGroups;
const filterMatches: ISettingMatch[] = [];
for (const group of allGroups) {
const groupMatched = groupFilter(group);
for (const section of group.sections) {
for (const setting of section.settings) {
const settingMatchResult = settingMatcher(setting, group);
if (groupMatched || settingMatchResult) {
filterMatches.push({
setting,
matches: settingMatchResult && settingMatchResult.matches,
score: settingMatchResult ? settingMatchResult.score : 0
});
}
}
}
}
return filterMatches.sort((a, b) => b.score - a.score);
}
public getPreference(key: string): ISetting {
for (const group of this.settingsGroups) {
for (const section of group.sections) {
for (const setting of section.settings) {
if (key === setting.key) {
return setting;
}
}
}
}
return null;
}
protected collectMetadata(groups: ISearchResultGroup[]): IStringDictionary<IFilterMetadata> {
const metadata = Object.create(null);
let hasMetadata = false;
groups.forEach(g => {
if (g.result.metadata) {
metadata[g.id] = g.result.metadata;
hasMetadata = true;
}
});
return hasMetadata ? metadata : null;
}
protected get filterGroups(): ISettingsGroup[] {
return this.settingsGroups;
}
public abstract settingsGroups: ISettingsGroup[];
public abstract findValueMatches(filter: string, setting: ISetting): IRange[];
protected abstract update(): IFilterResult;
}
export class SettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel {
private _settingsGroups: ISettingsGroup[];
protected settingsModel: ITextModel;
private readonly _onDidChangeGroups: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeGroups: Event<void> = this._onDidChangeGroups.event;
constructor(reference: IReference<ITextEditorModel>, private _configurationTarget: ConfigurationTarget) {
super();
this.settingsModel = reference.object.textEditorModel;
this._register(this.onDispose(() => reference.dispose()));
this._register(this.settingsModel.onDidChangeContent(() => {
this._settingsGroups = null;
this._onDidChangeGroups.fire();
}));
}
public get uri(): URI {
return this.settingsModel.uri;
}
public get configurationTarget(): ConfigurationTarget {
return this._configurationTarget;
}
public get settingsGroups(): ISettingsGroup[] {
if (!this._settingsGroups) {
this.parse();
}
return this._settingsGroups;
}
public get content(): string {
return this.settingsModel.getValue();
}
public findValueMatches(filter: string, setting: ISetting): IRange[] {
return this.settingsModel.findMatches(filter, setting.valueRange, false, false, null, false).map(match => match.range);
}
protected isSettingsProperty(property: string, previousParents: string[]): boolean {
return previousParents.length === 0; // Settings is root
}
protected parse(): void {
this._settingsGroups = parse(this.settingsModel, (property: string, previousParents: string[]): boolean => this.isSettingsProperty(property, previousParents));
}
protected update(): IFilterResult {
const resultGroups = map.values(this._currentResultGroups);
if (!resultGroups.length) {
return null;
}
// Transform resultGroups into IFilterResult - ISetting ranges are already correct here
const filteredSettings: ISetting[] = [];
const matches: IRange[] = [];
resultGroups.forEach(group => {
group.result.filterMatches.forEach(filterMatch => {
filteredSettings.push(filterMatch.setting);
matches.push(...filterMatch.matches);
});
});
let filteredGroup: ISettingsGroup;
const modelGroup = this.settingsGroups[0]; // Editable model has one or zero groups
if (modelGroup) {
filteredGroup = {
id: modelGroup.id,
range: modelGroup.range,
sections: [{
settings: filteredSettings
}],
title: modelGroup.title,
titleRange: modelGroup.titleRange
};
}
const metadata = this.collectMetadata(resultGroups);
return <IFilterResult>{
allGroups: this.settingsGroups,
filteredGroups: filteredGroup ? [filteredGroup] : [],
matches,
metadata
};
}
}
function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, previousParents: string[]) => boolean): ISettingsGroup[] {
const settings: ISetting[] = [];
let overrideSetting: ISetting = null;
let currentProperty: string = null;
let currentParent: any = [];
let previousParents: any[] = [];
let settingsPropertyIndex: number = -1;
let range = {
startLineNumber: 0,
startColumn: 0,
endLineNumber: 0,
endColumn: 0
};
function onValue(value: any, offset: number, length: number) {
if (Array.isArray(currentParent)) {
(<any[]>currentParent).push(value);
} else if (currentProperty) {
currentParent[currentProperty] = value;
}
if (previousParents.length === settingsPropertyIndex + 1 || (previousParents.length === settingsPropertyIndex + 2 && overrideSetting !== null)) {
// settings value started
const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting.overrides[overrideSetting.overrides.length - 1];
if (setting) {
let valueStartPosition = model.getPositionAt(offset);
let valueEndPosition = model.getPositionAt(offset + length);
setting.value = value;
setting.valueRange = {
startLineNumber: valueStartPosition.lineNumber,
startColumn: valueStartPosition.column,
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
};
setting.range = assign(setting.range, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
}
}
}
let visitor: JSONVisitor = {
onObjectBegin: (offset: number, length: number) => {
if (isSettingsProperty(currentProperty, previousParents)) {
// Settings started
settingsPropertyIndex = previousParents.length;
let position = model.getPositionAt(offset);
range.startLineNumber = position.lineNumber;
range.startColumn = position.column;
}
let object = {};
onValue(object, offset, length);
currentParent = object;
currentProperty = null;
previousParents.push(currentParent);
},
onObjectProperty: (name: string, offset: number, length: number) => {
currentProperty = name;
if (previousParents.length === settingsPropertyIndex + 1 || (previousParents.length === settingsPropertyIndex + 2 && overrideSetting !== null)) {
// setting started
let settingStartPosition = model.getPositionAt(offset);
const setting: ISetting = {
description: [],
key: name,
keyRange: {
startLineNumber: settingStartPosition.lineNumber,
startColumn: settingStartPosition.column + 1,
endLineNumber: settingStartPosition.lineNumber,
endColumn: settingStartPosition.column + length
},
range: {
startLineNumber: settingStartPosition.lineNumber,
startColumn: settingStartPosition.column,
endLineNumber: 0,
endColumn: 0
},
value: null,
valueRange: null,
descriptionRanges: null,
overrides: [],
overrideOf: overrideSetting
};
if (previousParents.length === settingsPropertyIndex + 1) {
settings.push(setting);
if (OVERRIDE_PROPERTY_PATTERN.test(name)) {
overrideSetting = setting;
}
} else {
overrideSetting.overrides.push(setting);
}
}
},
onObjectEnd: (offset: number, length: number) => {
currentParent = previousParents.pop();
if (previousParents.length === settingsPropertyIndex + 1 || (previousParents.length === settingsPropertyIndex + 2 && overrideSetting !== null)) {
// setting ended
const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting.overrides[overrideSetting.overrides.length - 1];
if (setting) {
let valueEndPosition = model.getPositionAt(offset + length);
setting.valueRange = assign(setting.valueRange, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
setting.range = assign(setting.range, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
}
if (previousParents.length === settingsPropertyIndex + 1) {
overrideSetting = null;
}
}
if (previousParents.length === settingsPropertyIndex) {
// settings ended
let position = model.getPositionAt(offset);
range.endLineNumber = position.lineNumber;
range.endColumn = position.column;
}
},
onArrayBegin: (offset: number, length: number) => {
let array: any[] = [];
onValue(array, offset, length);
previousParents.push(currentParent);
currentParent = array;
currentProperty = null;
},
onArrayEnd: (offset: number, length: number) => {
currentParent = previousParents.pop();
if (previousParents.length === settingsPropertyIndex + 1 || (previousParents.length === settingsPropertyIndex + 2 && overrideSetting !== null)) {
// setting value ended
const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting.overrides[overrideSetting.overrides.length - 1];
if (setting) {
let valueEndPosition = model.getPositionAt(offset + length);
setting.valueRange = assign(setting.valueRange, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
setting.range = assign(setting.range, {
endLineNumber: valueEndPosition.lineNumber,
endColumn: valueEndPosition.column
});
}
}
},
onLiteralValue: onValue,
onError: (error) => {
const setting = settings[settings.length - 1];
if (setting && (!setting.range || !setting.keyRange || !setting.valueRange)) {
settings.pop();
}
}
};
if (!model.isDisposed()) {
visit(model.getValue(), visitor);
}
return settings.length > 0 ? [<ISettingsGroup>{
sections: [
{
settings
}
],
title: null,
titleRange: null,
range
}] : [];
}
export class WorkspaceConfigurationEditorModel extends SettingsEditorModel {
private _configurationGroups: ISettingsGroup[];
get configurationGroups(): ISettingsGroup[] {
return this._configurationGroups;
}
protected parse(): void {
super.parse();
this._configurationGroups = parse(this.settingsModel, (property: string, previousParents: string[]): boolean => previousParents.length === 0);
}
protected isSettingsProperty(property: string, previousParents: string[]): boolean {
return property === 'settings' && previousParents.length === 1;
}
}
export class DefaultSettings extends Disposable {
private static _RAW: string;
private _allSettingsGroups: ISettingsGroup[];
private _content: string;
private _settingsByName: Map<string, ISetting>;
readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(
private _mostCommonlyUsedSettingsKeys: string[],
readonly target: ConfigurationTarget,
) {
super();
}
get content(): string {
if (!this._content) {
this.parse();
}
return this._content;
}
get settingsGroups(): ISettingsGroup[] {
if (!this._allSettingsGroups) {
this.parse();
}
return this._allSettingsGroups;
}
parse(): string {
const settingsGroups = this.getRegisteredGroups();
this.initAllSettingsMap(settingsGroups);
const mostCommonlyUsed = this.getMostCommonlyUsedSettings(settingsGroups);
this._allSettingsGroups = [mostCommonlyUsed, ...settingsGroups];
this._content = this.toContent(true, this._allSettingsGroups);
return this._content;
}
get raw(): string {
if (!DefaultSettings._RAW) {
DefaultSettings._RAW = this.toContent(false, this.getRegisteredGroups());
}
return DefaultSettings._RAW;
}
getSettingByName(name: string): ISetting {
return this._settingsByName && this._settingsByName.get(name);
}
private getRegisteredGroups(): ISettingsGroup[] {
const configurations = Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurations().slice();
return this.removeEmptySettingsGroups(configurations.sort(this.compareConfigurationNodes)
.reduce((result, config, index, array) => this.parseConfig(config, result, array), []));
}
private initAllSettingsMap(allSettingsGroups: ISettingsGroup[]): void {
this._settingsByName = new Map<string, ISetting>();
for (const group of allSettingsGroups) {
for (const section of group.sections) {
for (const setting of section.settings) {
this._settingsByName.set(setting.key, setting);
}
}
}
}
private getMostCommonlyUsedSettings(allSettingsGroups: ISettingsGroup[]): ISettingsGroup {
const settings = this._mostCommonlyUsedSettingsKeys.map(key => {
const setting = this._settingsByName.get(key);
if (setting) {
return <ISetting>{
description: setting.description,
key: setting.key,
value: setting.value,
range: null,
valueRange: null,
overrides: []
};
}
return null;
}).filter(setting => !!setting);
return <ISettingsGroup>{
id: 'mostCommonlyUsed',
range: null,
title: nls.localize('commonlyUsed', "Commonly Used"),
titleRange: null,
sections: [
{
settings
}
]
};
}
private parseConfig(config: IConfigurationNode, result: ISettingsGroup[], configurations: IConfigurationNode[], settingsGroup?: ISettingsGroup): ISettingsGroup[] {
let title = config.title;
if (!title) {
const configWithTitleAndSameId = configurations.filter(c => c.id === config.id && c.title)[0];
if (configWithTitleAndSameId) {
title = configWithTitleAndSameId.title;
}
}
if (title) {
if (!settingsGroup) {
settingsGroup = result.filter(g => g.title === title)[0];
if (!settingsGroup) {
settingsGroup = { sections: [{ settings: [] }], id: config.id, title: title, titleRange: null, range: null };
result.push(settingsGroup);
}
} else {
settingsGroup.sections[settingsGroup.sections.length - 1].title = title;
}
}
if (config.properties) {
if (!settingsGroup) {
settingsGroup = { sections: [{ settings: [] }], id: config.id, title: config.id, titleRange: null, range: null };
result.push(settingsGroup);
}
const configurationSettings: ISetting[] = [...settingsGroup.sections[settingsGroup.sections.length - 1].settings, ...this.parseSettings(config.properties)];
if (configurationSettings.length) {
configurationSettings.sort((a, b) => a.key.localeCompare(b.key));
settingsGroup.sections[settingsGroup.sections.length - 1].settings = configurationSettings;
}
}
if (config.allOf) {
config.allOf.forEach(c => this.parseConfig(c, result, configurations, settingsGroup));
}
return result;
}
private removeEmptySettingsGroups(settingsGroups: ISettingsGroup[]): ISettingsGroup[] {
const result = [];
for (const settingsGroup of settingsGroups) {
settingsGroup.sections = settingsGroup.sections.filter(section => section.settings.length > 0);
if (settingsGroup.sections.length) {
result.push(settingsGroup);
}
}
return result;
}
private parseSettings(settingsObject: { [path: string]: IConfigurationPropertySchema; }): ISetting[] {
let result = [];
for (let key in settingsObject) {
const prop = settingsObject[key];
if (!prop.deprecationMessage && this.matchesScope(prop)) {
const value = prop.default;
const description = (prop.description || '').split('\n');
const overrides = OVERRIDE_PROPERTY_PATTERN.test(key) ? this.parseOverrideSettings(prop.default) : [];
result.push({ key, value, description, range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides });
}
}
return result;
}
private parseOverrideSettings(overrideSettings: any): ISetting[] {
return Object.keys(overrideSettings).map((key) => ({ key, value: overrideSettings[key], description: [], range: null, keyRange: null, valueRange: null, descriptionRanges: [], overrides: [] }));
}
private matchesScope(property: IConfigurationNode): boolean {
if (this.target === ConfigurationTarget.WORKSPACE_FOLDER) {
return property.scope === ConfigurationScope.RESOURCE;
}
if (this.target === ConfigurationTarget.WORKSPACE) {
return property.scope === ConfigurationScope.WINDOW || property.scope === ConfigurationScope.RESOURCE;
}
return true;
}
private compareConfigurationNodes(c1: IConfigurationNode, c2: IConfigurationNode): number {
if (typeof c1.order !== 'number') {
return 1;
}
if (typeof c2.order !== 'number') {
return -1;
}
if (c1.order === c2.order) {
const title1 = c1.title || '';
const title2 = c2.title || '';
return title1.localeCompare(title2);
}
return c1.order - c2.order;
}
private toContent(asArray: boolean, settingsGroups: ISettingsGroup[]): string {
const builder = new SettingsContentBuilder();
if (asArray) {
builder.pushLine('[');
}
settingsGroups.forEach((settingsGroup, i) => {
builder.pushGroup(settingsGroup);
builder.pushLine(',');
});
if (asArray) {
builder.pushLine(']');
}
return builder.getContent();
}
}
export class DefaultSettingsEditorModel extends AbstractSettingsModel implements ISettingsEditorModel {
private _model: ITextModel;
private readonly _onDidChangeGroups: Emitter<void> = this._register(new Emitter<void>());
readonly onDidChangeGroups: Event<void> = this._onDidChangeGroups.event;
constructor(
private _uri: URI,
reference: IReference<ITextEditorModel>,
private readonly defaultSettings: DefaultSettings
) {
super();
this._register(defaultSettings.onDidChange(() => this._onDidChangeGroups.fire()));
this._model = reference.object.textEditorModel;
this._register(this.onDispose(() => reference.dispose()));
}
public get uri(): URI {
return this._uri;
}
public get target(): ConfigurationTarget {
return this.defaultSettings.target;
}
public get settingsGroups(): ISettingsGroup[] {
return this.defaultSettings.settingsGroups;
}
protected get filterGroups(): ISettingsGroup[] {
// Don't look at "commonly used" for filter
return this.settingsGroups.slice(1);
}
protected update(): IFilterResult {
// Grab current result groups, only render non-empty groups
const resultGroups = map
.values(this._currentResultGroups)
.sort((a, b) => a.order - b.order);
const nonEmptyResultGroups = resultGroups.filter(group => group.result.filterMatches.length);
const startLine = tail(this.settingsGroups).range.endLineNumber + 2;
const { settingsGroups: filteredGroups, matches } = this.writeResultGroups(nonEmptyResultGroups, startLine);
const metadata = this.collectMetadata(resultGroups);
return resultGroups.length ?
<IFilterResult>{
allGroups: this.settingsGroups,
filteredGroups,
matches,
metadata
} :
null;
}
/**
* Translate the ISearchResultGroups to text, and write it to the editor model
*/
private writeResultGroups(groups: ISearchResultGroup[], startLine: number): { matches: IRange[], settingsGroups: ISettingsGroup[] } {
const contentBuilderOffset = startLine - 1;
const builder = new SettingsContentBuilder(contentBuilderOffset);
const settingsGroups: ISettingsGroup[] = [];
const matches: IRange[] = [];
builder.pushLine(',');
groups.forEach(resultGroup => {
const settingsGroup = this.getGroup(resultGroup);
settingsGroups.push(settingsGroup);
matches.push(...this.writeSettingsGroupToBuilder(builder, settingsGroup, resultGroup.result.filterMatches));
});
// note: 1-indexed line numbers here
const groupContent = builder.getContent() + '\n';
const groupEndLine = this._model.getLineCount();
const cursorPosition = new Selection(startLine, 1, startLine, 1);
const edit: IIdentifiedSingleEditOperation = {
text: groupContent,
forceMoveMarkers: true,
range: new Range(startLine, 1, groupEndLine, 1),
identifier: { major: 1, minor: 0 }
};
this._model.pushEditOperations([cursorPosition], [edit], () => [cursorPosition]);
// Force tokenization now - otherwise it may be slightly delayed, causing a flash of white text
const tokenizeTo = Math.min(startLine + 60, this._model.getLineCount());
this._model.forceTokenization(tokenizeTo);
return { matches, settingsGroups };
}
private writeSettingsGroupToBuilder(builder: SettingsContentBuilder, settingsGroup: ISettingsGroup, filterMatches: ISettingMatch[]): IRange[] {
filterMatches = filterMatches
.map(filteredMatch => {
// Fix match ranges to offset from setting start line
return <ISettingMatch>{
setting: filteredMatch.setting,
score: filteredMatch.score,
matches: filteredMatch.matches && filteredMatch.matches.map(match => {
return new Range(
match.startLineNumber - filteredMatch.setting.range.startLineNumber,
match.startColumn,
match.endLineNumber - filteredMatch.setting.range.startLineNumber,
match.endColumn);
})
};
});
builder.pushGroup(settingsGroup);
builder.pushLine(',');
// builder has rewritten settings ranges, fix match ranges
const fixedMatches = flatten(
filterMatches
.map(m => m.matches || [])
.map((settingMatches, i) => {
const setting = settingsGroup.sections[0].settings[i];
return settingMatches.map(range => {
return new Range(
range.startLineNumber + setting.range.startLineNumber,
range.startColumn,
range.endLineNumber + setting.range.startLineNumber,
range.endColumn);
});
}));
return fixedMatches;
}
private copySetting(setting: ISetting): ISetting {
return <ISetting>{
description: setting.description,
key: setting.key,
value: setting.value,
range: setting.range,
overrides: [],
overrideOf: setting.overrideOf
};
}
public findValueMatches(filter: string, setting: ISetting): IRange[] {
return [];
}
public getPreference(key: string): ISetting {
for (const group of this.settingsGroups) {
for (const section of group.sections) {
for (const setting of section.settings) {
if (setting.key === key) {
return setting;
}
}
}
}
return null;
}
private getGroup(resultGroup: ISearchResultGroup): ISettingsGroup {
return <ISettingsGroup>{
id: resultGroup.id,
range: null,
title: resultGroup.label,
titleRange: null,
sections: [
{
settings: resultGroup.result.filterMatches.map(m => this.copySetting(m.setting))
}
]
};
}
}
class SettingsContentBuilder {
private _contentByLines: string[];
private get lineCountWithOffset(): number {
return this._contentByLines.length + this._rangeOffset;
}
private get lastLine(): string {
return this._contentByLines[this._contentByLines.length - 1] || '';
}
constructor(private _rangeOffset = 0) {
this._contentByLines = [];
}
private offsetIndexToIndex(offsetIdx: number): number {
return offsetIdx - this._rangeOffset;
}
pushLine(...lineText: string[]): void {
this._contentByLines.push(...lineText);
}
pushGroup(settingsGroups: ISettingsGroup): void {
this._contentByLines.push('{');
this._contentByLines.push('');
this._contentByLines.push('');
const lastSetting = this._pushGroup(settingsGroups);
if (lastSetting) {
// Strip the comma from the last setting
const lineIdx = this.offsetIndexToIndex(lastSetting.range.endLineNumber);
const content = this._contentByLines[lineIdx - 2];
this._contentByLines[lineIdx - 2] = content.substring(0, content.length - 1);
}
this._contentByLines.push('}');
}
private _pushGroup(group: ISettingsGroup): ISetting {
const indent = ' ';
let lastSetting: ISetting = null;
let groupStart = this.lineCountWithOffset + 1;
for (const section of group.sections) {
if (section.title) {
let sectionTitleStart = this.lineCountWithOffset + 1;
this.addDescription([section.title], indent, this._contentByLines);
section.titleRange = { startLineNumber: sectionTitleStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length };
}
if (section.settings.length) {
for (const setting of section.settings) {
this.pushSetting(setting, indent);
lastSetting = setting;
}
}
}
group.range = { startLineNumber: groupStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length };
return lastSetting;
}
getContent(): string {
return this._contentByLines.join('\n');
}
private pushSetting(setting: ISetting, indent: string): void {
const settingStart = this.lineCountWithOffset + 1;
setting.descriptionRanges = [];
const descriptionPreValue = indent + '// ';
for (const line of setting.description) {
this._contentByLines.push(descriptionPreValue + line);
setting.descriptionRanges.push({ startLineNumber: this.lineCountWithOffset, startColumn: this.lastLine.indexOf(line) + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length });
}
let preValueConent = indent;
const keyString = JSON.stringify(setting.key);
preValueConent += keyString;
setting.keyRange = { startLineNumber: this.lineCountWithOffset + 1, startColumn: preValueConent.indexOf(setting.key) + 1, endLineNumber: this.lineCountWithOffset + 1, endColumn: setting.key.length };
preValueConent += ': ';
const valueStart = this.lineCountWithOffset + 1;
this.pushValue(setting, preValueConent, indent);
setting.valueRange = { startLineNumber: valueStart, startColumn: preValueConent.length + 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length + 1 };
this._contentByLines[this._contentByLines.length - 1] += ',';
this._contentByLines.push('');
setting.range = { startLineNumber: settingStart, startColumn: 1, endLineNumber: this.lineCountWithOffset, endColumn: this.lastLine.length };
}
private pushValue(setting: ISetting, preValueConent: string, indent: string): void {
let valueString = JSON.stringify(setting.value, null, indent);
if (valueString && (typeof setting.value === 'object')) {
if (setting.overrides.length) {
this._contentByLines.push(preValueConent + ' {');
for (const subSetting of setting.overrides) {
this.pushSetting(subSetting, indent + indent);
this._contentByLines.pop();
}
const lastSetting = setting.overrides[setting.overrides.length - 1];
const content = this._contentByLines[lastSetting.range.endLineNumber - 2];
this._contentByLines[lastSetting.range.endLineNumber - 2] = content.substring(0, content.length - 1);
this._contentByLines.push(indent + '}');
} else {
const mulitLineValue = valueString.split('\n');
this._contentByLines.push(preValueConent + mulitLineValue[0]);
for (let i = 1; i < mulitLineValue.length; i++) {
this._contentByLines.push(indent + mulitLineValue[i]);
}
}
} else {
this._contentByLines.push(preValueConent + valueString);
}
}
private addDescription(description: string[], indent: string, result: string[]) {
for (const line of description) {
result.push(indent + '// ' + line);
}
}
}
export function defaultKeybindingsContents(keybindingService: IKeybindingService): string {
const defaultsHeader = '// ' + nls.localize('defaultKeybindingsHeader', "Overwrite key bindings by placing them into your key bindings file.");
return defaultsHeader + '\n' + keybindingService.getDefaultKeybindingsContent();
}
export class DefaultKeybindingsEditorModel implements IKeybindingsEditorModel<any> {
private _content: string;
constructor(private _uri: URI,
@IKeybindingService private keybindingService: IKeybindingService) {
}
public get uri(): URI {
return this._uri;
}
public get content(): string {
if (!this._content) {
this._content = defaultKeybindingsContents(this.keybindingService);
}
return this._content;
}
public getPreference(): any {
return null;
}
public dispose(): void {
// Not disposable
}
}

View File

@@ -0,0 +1,655 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as uuid from 'vs/base/common/uuid';
import { TPromise } from 'vs/base/common/winjs.base';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { Registry } from 'vs/platform/registry/common/platform';
import { Action } from 'vs/base/common/actions';
import { KeyCode, SimpleKeybinding, ChordKeybinding } from 'vs/base/common/keyCodes';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingsEditorModel, IKeybindingItemEntry } from 'vs/workbench/services/preferences/common/keybindingsEditorModel';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
interface Modifiers {
metaKey?: boolean;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
}
class AnAction extends Action {
constructor(id: string) {
super(id);
}
}
suite('Keybindings Editor Model test', () => {
let instantiationService: TestInstantiationService;
let testObject: KeybindingsEditorModel;
setup(() => {
instantiationService = new TestInstantiationService();
instantiationService.stub(IKeybindingService, {});
instantiationService.stub(IExtensionService, {}, 'whenInstalledExtensionsRegistered', () => TPromise.as(null));
testObject = instantiationService.createInstance(KeybindingsEditorModel, OS);
CommandsRegistry.registerCommand('command_without_keybinding', () => { });
});
test('fetch returns default keybindings', () => {
const expected = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } })
);
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns default keybindings at the top', () => {
const expected = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } })
);
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch('').slice(0, 2), true);
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns default keybindings sorted by command id', () => {
const keybindings = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'c' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Backspace } })
);
const expected = [keybindings[2], keybindings[0], keybindings[1]];
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns user keybinding first if default and user has same id', () => {
const sameId = 'b' + uuid.generateUuid();
const keybindings = prepareKeybindingService(
aResolvedKeybindingItem({ command: sameId, firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: sameId, firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape }, isDefault: false })
);
const expected = [keybindings[1], keybindings[0]];
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns keybinding with titles first', () => {
const keybindings = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'c' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'd' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } })
);
registerCommandWithTitle(keybindings[1].command, 'B Title');
registerCommandWithTitle(keybindings[3].command, 'A Title');
const expected = [keybindings[3], keybindings[1], keybindings[0], keybindings[2]];
instantiationService.stub(IKeybindingService, 'getKeybindings', () => keybindings);
instantiationService.stub(IKeybindingService, 'getDefaultKeybindings', () => keybindings);
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns keybinding with user first if title and id matches', () => {
const sameId = 'b' + uuid.generateUuid();
const keybindings = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: sameId, firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'c' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: sameId, firstPart: { keyCode: KeyCode.Escape }, isDefault: false })
);
registerCommandWithTitle(keybindings[1].command, 'Same Title');
registerCommandWithTitle(keybindings[3].command, 'Same Title');
const expected = [keybindings[3], keybindings[1], keybindings[0], keybindings[2]];
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch(''));
assertKeybindingItems(actuals, expected);
});
});
test('fetch returns default keybindings sorted by precedence', () => {
const expected = prepareKeybindingService(
aResolvedKeybindingItem({ command: 'b' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'c' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, chordPart: { keyCode: KeyCode.Escape } }),
aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Backspace } })
);
return testObject.resolve({}).then(() => {
const actuals = asResolvedKeybindingItems(testObject.fetch('', true));
assertKeybindingItems(actuals, expected);
});
});
test('convert keybinding without title to entry', () => {
const expected = aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2' });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('')[0];
assert.equal(actual.keybindingItem.command, expected.command);
assert.equal(actual.keybindingItem.commandLabel, '');
assert.equal(actual.keybindingItem.commandDefaultLabel, null);
assert.equal(actual.keybindingItem.keybinding.getAriaLabel(), expected.resolvedKeybinding.getAriaLabel());
assert.equal(actual.keybindingItem.when, expected.when.serialize());
});
});
test('convert keybinding with title to entry', () => {
const expected = aResolvedKeybindingItem({ command: 'a' + uuid.generateUuid(), firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2' });
prepareKeybindingService(expected);
registerCommandWithTitle(expected.command, 'Some Title');
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('')[0];
assert.equal(actual.keybindingItem.command, expected.command);
assert.equal(actual.keybindingItem.commandLabel, 'Some Title');
assert.equal(actual.keybindingItem.commandDefaultLabel, null);
assert.equal(actual.keybindingItem.keybinding.getAriaLabel(), expected.resolvedKeybinding.getAriaLabel());
assert.equal(actual.keybindingItem.when, expected.when.serialize());
});
});
test('convert without title and binding to entry', () => {
CommandsRegistry.registerCommand('command_without_keybinding', () => { });
prepareKeybindingService();
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('').filter(element => element.keybindingItem.command === 'command_without_keybinding')[0];
assert.equal(actual.keybindingItem.command, 'command_without_keybinding');
assert.equal(actual.keybindingItem.commandLabel, '');
assert.equal(actual.keybindingItem.commandDefaultLabel, null);
assert.equal(actual.keybindingItem.keybinding, null);
assert.equal(actual.keybindingItem.when, '');
});
});
test('convert with title and wihtout binding to entry', () => {
const id = 'a' + uuid.generateUuid();
registerCommandWithTitle(id, 'some title');
prepareKeybindingService();
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('').filter(element => element.keybindingItem.command === id)[0];
assert.equal(actual.keybindingItem.command, id);
assert.equal(actual.keybindingItem.commandLabel, 'some title');
assert.equal(actual.keybindingItem.commandDefaultLabel, null);
assert.equal(actual.keybindingItem.keybinding, null);
assert.equal(actual.keybindingItem.when, '');
});
});
test('filter by command id', () => {
const id = 'workbench.action.increaseViewSize';
registerCommandWithTitle(id, 'some title');
prepareKeybindingService();
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('workbench action view size').filter(element => element.keybindingItem.command === id)[0];
assert.ok(actual);
});
});
test('filter by command title', () => {
const id = 'a' + uuid.generateUuid();
registerCommandWithTitle(id, 'Increase view size');
prepareKeybindingService();
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('increase size').filter(element => element.keybindingItem.command === id)[0];
assert.ok(actual);
});
});
test('filter by default source', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2' });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('default').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by user source', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2', isDefault: false });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('user').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by default source with "@source: " prefix', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2', isDefault: true });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('@source: default').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by user source with "@source: " prefix', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'context1 && context2', isDefault: false });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('@source: user').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by when context', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('when context').filter(element => element.keybindingItem.command === command)[0];
assert.ok(actual);
});
});
test('filter by cmd key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected);
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by meta key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('meta').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by command key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('command').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by windows key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Windows);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('windows').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by alt key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('alt').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { altKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by option key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('option').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { altKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by ctrl key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('ctrl').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { ctrlKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by control key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('control').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { ctrlKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by shift key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('shift').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { shiftKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by arrow', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { shiftKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('arrow').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by modifier and key', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('alt right').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { altKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by key and modifier', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { altKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.RightArrow, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('right alt').filter(element => element.keybindingItem.command === command);
assert.equal(0, actual.length);
});
});
test('filter by modifiers and key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true, metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('alt cmd esc').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { altKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by modifiers in random order and key', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd shift esc').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true, shiftKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter by first part', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd shift esc').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true, shiftKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter matches in chord part', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd del').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { metaKey: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, { keyCode: true });
});
});
test('filter matches first part and in chord part', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.Delete }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.UpArrow }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('cmd shift esc del').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { shiftKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, { keyCode: true });
});
});
test('filter exact matches', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"ctrl c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { ctrlKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter exact matches with first and chord part', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"shift meta escape ctrl c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { shiftKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, { ctrlKey: true, keyCode: true });
});
});
test('filter exact matches with first and chord part no results', () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.Delete, modifiers: { metaKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.UpArrow }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"cmd shift esc del"').filter(element => element.keybindingItem.command === command);
assert.equal(0, actual.length);
});
});
test('filter matches with + separator', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"control+c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { ctrlKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, {});
});
});
test('filter matches with + separator in first and chord parts', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { shiftKey: true, metaKey: true } }, chordPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.KEY_C, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"shift+meta+escape ctrl+c"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches.firstPart, { shiftKey: true, metaKey: true, keyCode: true });
assert.deepEqual(actual[0].keybindingMatches.chordPart, { keyCode: true, ctrlKey: true });
});
});
test('filter exact matches with space #32993', () => {
const command = 'a' + uuid.generateUuid();
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Space, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Backspace, modifiers: { ctrlKey: true } }, when: 'whenContext1 && whenContext2', isDefault: false }));
return testObject.resolve({}).then(() => {
const actual = testObject.fetch('"ctrl+space"').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
});
});
function prepareKeybindingService(...keybindingItems: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {
instantiationService.stub(IKeybindingService, 'getKeybindings', () => keybindingItems);
instantiationService.stub(IKeybindingService, 'getDefaultKeybindings', () => keybindingItems);
return keybindingItems;
}
function registerCommandWithTitle(command: string, title: string): void {
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(AnAction, command, title, { primary: null }), '');
}
function assertKeybindingItems(actual: ResolvedKeybindingItem[], expected: ResolvedKeybindingItem[]) {
assert.equal(actual.length, expected.length);
for (let i = 0; i < actual.length; i++) {
assertKeybindingItem(actual[i], expected[i]);
}
}
function assertKeybindingItem(actual: ResolvedKeybindingItem, expected: ResolvedKeybindingItem): void {
assert.equal(actual.command, expected.command);
if (actual.when) {
assert.ok(!!expected.when);
assert.equal(actual.when.serialize(), expected.when.serialize());
} else {
assert.ok(!expected.when);
}
assert.equal(actual.isDefault, expected.isDefault);
if (actual.resolvedKeybinding) {
assert.ok(!!expected.resolvedKeybinding);
assert.equal(actual.resolvedKeybinding.getLabel(), expected.resolvedKeybinding.getLabel());
} else {
assert.ok(!expected.resolvedKeybinding);
}
}
function aResolvedKeybindingItem({ command, when, isDefault, firstPart, chordPart }: { command?: string, when?: string, isDefault?: boolean, firstPart?: { keyCode: KeyCode, modifiers?: Modifiers }, chordPart?: { keyCode: KeyCode, modifiers?: Modifiers } }): ResolvedKeybindingItem {
const aSimpleKeybinding = function (part: { keyCode: KeyCode, modifiers?: Modifiers }): SimpleKeybinding {
const { ctrlKey, shiftKey, altKey, metaKey } = part.modifiers || { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false };
return new SimpleKeybinding(ctrlKey, shiftKey, altKey, metaKey, part.keyCode);
};
const keybinding = firstPart ? chordPart ? new ChordKeybinding(aSimpleKeybinding(firstPart), aSimpleKeybinding(chordPart)) : aSimpleKeybinding(firstPart) : null;
return new ResolvedKeybindingItem(keybinding ? new USLayoutResolvedKeybinding(keybinding, OS) : null, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : null, isDefault === void 0 ? true : isDefault);
}
function asResolvedKeybindingItems(keybindingEntries: IKeybindingItemEntry[], keepUnassigned: boolean = false): ResolvedKeybindingItem[] {
if (!keepUnassigned) {
keybindingEntries = keybindingEntries.filter(keybindingEntry => !!keybindingEntry.keybindingItem.keybinding);
}
return keybindingEntries.map(entry => entry.keybindingItem.keybindingItem);
}
});

View File

@@ -1,31 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
.monaco-workbench > .part.statusbar > .statusbar-item.progress {
background-image: url(progress.svg);
background-repeat: no-repeat;
background-position: left;
padding-left: 14px;
display: inline;
padding-left: 5px;
}
.monaco-workbench > .part.statusbar > .statusbar-item.progress .spinner-container {
padding-right: 5px;
}
.monaco-workbench .progress-badge > .badge-content {

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import lifecycle = require('vs/base/common/lifecycle');
import * as lifecycle from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import types = require('vs/base/common/types');
import * as types from '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';
@@ -102,17 +102,17 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
// Replay Infinite Progress
else if (this.progressState.infinite) {
this.progressbar.infinite().getContainer().show();
this.progressbar.infinite().show();
}
// Replay Finite Progress (Total & Worked)
else {
if (this.progressState.total) {
this.progressbar.total(this.progressState.total).getContainer().show();
this.progressbar.total(this.progressState.total).show();
}
if (this.progressState.worked) {
this.progressbar.worked(this.progressState.worked).getContainer().show();
this.progressbar.worked(this.progressState.worked).show();
}
}
}
@@ -152,20 +152,12 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
// 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);
}
this.progressbar.infinite().show(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);
}
this.progressbar.total(total).show(delay);
}
}
@@ -200,7 +192,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
this.progressState.infinite = true;
this.progressState.worked = void 0;
this.progressState.total = void 0;
this.progressbar.infinite().getContainer().show();
this.progressbar.infinite().show();
}
},
@@ -209,7 +201,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
this.progressState.done = true;
if (this.isActive) {
this.progressbar.stop().getContainer().hide();
this.progressbar.stop().hide();
}
}
};
@@ -244,7 +236,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
this.clearProgressState();
if (this.isActive) {
this.progressbar.stop().getContainer().hide();
this.progressbar.stop().hide();
}
};
@@ -257,11 +249,7 @@ export class WorkbenchProgressService extends ScopedService implements IProgress
// Show Progress when active
if (this.isActive) {
if (types.isUndefinedOrNull(delay)) {
this.progressbar.infinite().getContainer().show();
} else {
this.progressbar.infinite().getContainer().showDelayed(delay);
}
this.progressbar.infinite().show(delay);
}
}

View File

@@ -16,6 +16,9 @@ import { StatusbarAlignment, IStatusbarRegistry, StatusbarItemDescriptor, Extens
import { TPromise } from 'vs/base/common/winjs.base';
import { always } from 'vs/base/common/async';
import { ProgressBadge, IActivityService } from 'vs/workbench/services/activity/common/activity';
import { INotificationService, Severity, INotificationHandle, INotificationActions } from 'vs/platform/notification/common/notification';
import { Action } from 'vs/base/common/actions';
import { once } from 'vs/base/common/event';
class WindowProgressItem implements IStatusbarItem {
@@ -30,9 +33,25 @@ class WindowProgressItem implements IStatusbarItem {
render(element: HTMLElement): IDisposable {
this._element = element;
this._label = new OcticonLabel(this._element);
this._element.classList.add('progress');
const container = document.createElement('span');
this._element.appendChild(container);
const spinnerContainer = document.createElement('span');
spinnerContainer.classList.add('spinner-container');
container.appendChild(spinnerContainer);
const spinner = new OcticonLabel(spinnerContainer);
spinner.text = '$(sync~spin)';
const labelContainer = document.createElement('span');
container.appendChild(labelContainer);
this._label = new OcticonLabel(labelContainer);
this.hide();
return null;
}
@@ -62,15 +81,18 @@ export class ProgressService2 implements IProgressService2 {
constructor(
@IActivityService private readonly _activityBar: IActivityService,
@IViewletService private readonly _viewletService: IViewletService
@IViewletService private readonly _viewletService: IViewletService,
@INotificationService private readonly _notificationService: INotificationService
) {
//
}
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P): P {
withProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => P, onDidCancel?: () => void): P {
const { location } = options;
switch (location) {
case ProgressLocation.Notification:
return this._withNotificationProgress(options, task, onDidCancel);
case ProgressLocation.Window:
return this._withWindowProgress(options, task);
case ProgressLocation.Explorer:
@@ -85,7 +107,7 @@ export class ProgressService2 implements IProgressService2 {
}
}
private _withWindowProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, percentage?: number }>) => P): P {
private _withWindowProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string }>) => P): P {
const task: [IProgressOptions, Progress<IProgressStep>] = [options, new Progress<IProgressStep>(() => this._updateWindowProgress())];
@@ -134,8 +156,8 @@ export class ProgressService2 implements IProgressService2 {
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);
if (options.source) {
title = localize('progress.title', "{0}: {1}", options.source, title);
}
WindowProgressItem.Instance.text = text;
@@ -144,7 +166,95 @@ export class ProgressService2 implements IProgressService2 {
}
}
private _withViewletProgress<P extends Thenable<R>, R=any>(viewletId: string, task: (progress: IProgress<{ message?: string, percentage?: number }>) => P): P {
private _withNotificationProgress<P extends Thenable<R>, R=any>(options: IProgressOptions, callback: (progress: IProgress<{ message?: string, increment?: number }>) => P, onDidCancel?: () => void): P {
const toDispose: IDisposable[] = [];
const createNotification = (message: string, increment?: number): INotificationHandle => {
if (!message) {
return undefined; // we need a message at least
}
const actions: INotificationActions = { primary: [] };
if (options.cancellable) {
const cancelAction = new class extends Action {
constructor() {
super('progress.cancel', localize('cancel', "Cancel"), null, true);
}
run(): TPromise<any> {
if (typeof onDidCancel === 'function') {
onDidCancel();
}
return TPromise.as(undefined);
}
};
toDispose.push(cancelAction);
actions.primary.push(cancelAction);
}
const handle = this._notificationService.notify({
severity: Severity.Info,
message: options.title,
source: options.source,
actions
});
updateProgress(handle, increment);
once(handle.onDidClose)(() => {
dispose(toDispose);
});
return handle;
};
const updateProgress = (notification: INotificationHandle, increment?: number): void => {
if (typeof increment === 'number' && increment >= 0) {
notification.progress.total(100); // always percentage based
notification.progress.worked(increment);
} else {
notification.progress.infinite();
}
};
let handle: INotificationHandle;
const updateNotification = (message?: string, increment?: number): void => {
if (!handle) {
handle = createNotification(message, increment);
} else {
if (typeof message === 'string') {
handle.updateMessage(message);
}
if (typeof increment === 'number') {
updateProgress(handle, increment);
}
}
};
// Show initially
updateNotification(options.title);
// Update based on progress
const p = callback({
report: progress => {
updateNotification(progress.message, progress.increment);
}
});
// Show progress for at least 800ms and then hide once done or canceled
always(TPromise.join([TPromise.timeout(800), p]), () => {
if (handle) {
handle.close();
}
});
return p;
}
private _withViewletProgress<P extends Thenable<R>, R=any>(viewletId: string, task: (progress: IProgress<{ message?: string }>) => P): P {
const promise = task(emptyProgress);

View File

@@ -22,10 +22,12 @@ let activeViewlet: Viewlet = {} as any;
class TestViewletService implements IViewletService {
public _serviceBrand: any;
onDidViewletRegisterEmitter = new Emitter<ViewletDescriptor>();
onDidViewletOpenEmitter = new Emitter<IViewlet>();
onDidViewletCloseEmitter = new Emitter<IViewlet>();
onDidViewletEnableEmitter = new Emitter<{ id: string, enabled: boolean }>();
onDidViewletRegister = this.onDidViewletRegisterEmitter.event;
onDidViewletOpen = this.onDidViewletOpenEmitter.event;
onDidViewletClose = this.onDidViewletCloseEmitter.event;
onDidViewletEnablementChange = this.onDidViewletEnableEmitter.event;
@@ -212,11 +214,12 @@ class TestProgressBar {
return this.done();
}
public getContainer() {
return {
show: function () { },
hide: function () { }
};
public show(): void {
}
public hide(): void {
}
}
@@ -280,12 +283,12 @@ suite('Progress Service', () => {
// Acive: Show While
let p = TPromise.as(null);
service.showWhile(p).then(() => {
return service.showWhile(p).then(() => {
assert.strictEqual(true, testProgressBar.fDone);
viewletService.onDidViewletCloseEmitter.fire(testViewlet);
p = TPromise.as(null);
service.showWhile(p).then(() => {
return service.showWhile(p).then(() => {
assert.strictEqual(true, testProgressBar.fDone);
viewletService.onDidViewletOpenEmitter.fire(testViewlet);

View File

@@ -8,7 +8,7 @@
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 { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { Command } from 'vs/editor/common/modes';
import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';

View File

@@ -6,7 +6,7 @@
'use strict';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import Event, { Emitter } from 'vs/base/common/event';
import { Event, Emitter } from 'vs/base/common/event';
import { ISCMService, ISCMProvider, ISCMInput, ISCMRepository, IInputValidator } from './scm';
import { ILogService } from 'vs/platform/log/common/log';
import { TPromise } from 'vs/base/common/winjs.base';
@@ -92,7 +92,7 @@ export class SCMService implements ISCMService {
private _onDidRemoveProvider = new Emitter<ISCMRepository>();
get onDidRemoveRepository(): Event<ISCMRepository> { return this._onDidRemoveProvider.event; }
constructor( @ILogService private logService: ILogService) { }
constructor(@ILogService private logService: ILogService) { }
registerSCMProvider(provider: ISCMProvider): ISCMRepository {
this.logService.trace('SCMService#registerSCMProvider');

View File

@@ -8,22 +8,22 @@
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 * as fs from 'fs';
import * as path from 'path';
import { isEqualOrParent } from 'vs/base/common/paths';
import { Readable } from 'stream';
import { TPromise } from 'vs/base/common/winjs.base';
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 * as objects from 'vs/base/common/objects';
import * as arrays from 'vs/base/common/arrays';
import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import * as types from 'vs/base/common/types';
import * as glob from '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 * as extfs from 'vs/base/node/extfs';
import * as flow from 'vs/base/node/flow';
import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search';
import { spawnRipgrepCmd } from './ripgrepFileSearch';
import { rgErrorMsgForDisplay } from './ripgrepTextSearch';

View File

@@ -5,15 +5,15 @@
'use strict';
import fs = require('fs');
import { isAbsolute, sep } from 'path';
import * as fs from 'fs';
import { isAbsolute, sep, join } from 'path';
import gracefulFs = require('graceful-fs');
import * as gracefulFs from 'graceful-fs';
gracefulFs.gracefulify(fs);
import arrays = require('vs/base/common/arrays');
import objects = require('vs/base/common/objects');
import strings = require('vs/base/common/strings');
import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import * as strings from '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/node/files';
@@ -22,7 +22,6 @@ import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/te
import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider';
import { IRawSearchService, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine, IFileSearchProgressItem, ITelemetryEvent } from './search';
import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search';
import { fuzzyContains } from 'vs/base/common/strings';
import { compareItemsByScore, IItemAccessor, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer';
export class SearchService implements IRawSearchService {
@@ -133,7 +132,7 @@ export class SearchService implements IRawSearchService {
}
private rawMatchToSearchItem(match: IRawFileMatch): ISerializedFileMatch {
return { path: match.base ? [match.base, match.relativePath].join(sep) : match.relativePath };
return { path: match.base ? join(match.base, match.relativePath) : match.relativePath };
}
private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
@@ -144,6 +143,7 @@ export class SearchService implements IRawSearchService {
.then(result => {
c([result, results]);
if (this.telemetryPipe) {
// __GDPR__TODO__ classify event
this.telemetryPipe({
eventName: 'fileSearch',
data: result.stats
@@ -310,7 +310,7 @@ export class SearchService implements IRawSearchService {
let entry = cachedEntries[i];
// Check if this entry is a match for the search value
if (!fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) {
if (!strings.fuzzyContains(entry.relativePath, normalizedSearchValueLowercase)) {
continue;
}

View File

@@ -32,14 +32,29 @@ function getRgArgs(config: IRawSearch, folderQuery: IFolderSearch, includePatter
// includePattern can't have siblingClauses
foldersToIncludeGlobs([folderQuery], includePattern, false).forEach(globArg => {
args.push('-g', anchor(isMac ? normalizeNFD(globArg) : globArg));
const inclusion = anchor(globArg);
args.push('-g', inclusion);
if (isMac) {
const normalized = normalizeNFD(inclusion);
if (normalized !== inclusion) {
args.push('-g', normalized);
}
}
});
let siblingClauses: glob.IExpression;
const rgGlobs = foldersToRgExcludeGlobs([folderQuery], excludePattern, undefined, false);
rgGlobs.globArgs
.forEach(rgGlob => args.push('-g', `!${anchor(isMac ? normalizeNFD(rgGlob) : rgGlob)}`));
rgGlobs.globArgs.forEach(globArg => {
const exclusion = `!${anchor(globArg)}`;
args.push('-g', exclusion);
if (isMac) {
const normalized = normalizeNFD(exclusion);
if (normalized !== exclusion) {
args.push('-g', normalized);
}
}
});
siblingClauses = rgGlobs.siblingClauses;
if (folderQuery.disregardIgnoreFiles !== false) {

View File

@@ -11,8 +11,8 @@ import { StringDecoder, NodeStringDecoder } from 'string_decoder';
import * as cp from 'child_process';
import { rgPath } from 'vscode-ripgrep';
import objects = require('vs/base/common/objects');
import platform = require('vs/base/common/platform');
import * as objects from 'vs/base/common/objects';
import * as platform from 'vs/base/common/platform';
import * as strings from 'vs/base/common/strings';
import * as paths from 'vs/base/common/paths';
import * as extfs from 'vs/base/node/extfs';
@@ -29,7 +29,7 @@ const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.u
export class RipgrepEngine {
private isDone = false;
private rgProc: cp.ChildProcess;
private killRgProcFn: Function;
private killRgProcFn: (code?: number) => void;
private postProcessExclusions: glob.ParsedExpression;
private ripgrepParser: RipgrepParser;
@@ -169,11 +169,11 @@ export function rgErrorMsgForDisplay(msg: string): string | undefined {
}
export class RipgrepParser extends EventEmitter {
private static readonly RESULT_REGEX = /^\u001b\[m(\d+)\u001b\[m:(.*)(\r?)/;
private static readonly FILE_REGEX = /^\u001b\[m(.+)\u001b\[m$/;
private static readonly RESULT_REGEX = /^\u001b\[0m(\d+)\u001b\[0m:(.*)(\r?)/;
private static readonly FILE_REGEX = /^\u001b\[0m(.+)\u001b\[0m$/;
public static readonly MATCH_START_MARKER = '\u001b[m\u001b[31m';
public static readonly MATCH_END_MARKER = '\u001b[m';
public static readonly MATCH_START_MARKER = '\u001b[0m\u001b[31m';
public static readonly MATCH_END_MARKER = '\u001b[0m';
private fileMatch: FileMatch;
private remainder: string;

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