mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-02 01:25:39 -05:00
Merge VS Code 1.23.1 (#1520)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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 })) };
|
||||
@@ -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>>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
146
src/vs/workbench/services/files/electron-browser/encoding.ts
Normal file
146
src/vs/workbench/services/files/electron-browser/encoding.ts
Normal 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
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
168
src/vs/workbench/services/files/electron-browser/streams.ts
Normal file
168
src/vs/workbench/services/files/electron-browser/streams.ts
Normal 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
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 274 B |
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>();
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -14,4 +14,5 @@ export const IWorkbenchIssueService = createDecorator<IWorkbenchIssueService>('w
|
||||
export interface IWorkbenchIssueService {
|
||||
_serviceBrand: any;
|
||||
openReporter(dataOverrides?: Partial<IssueReporterData>): TPromise<void>;
|
||||
openProcessExplorer(): TPromise<void>;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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/));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
164
src/vs/workbench/services/preferences/common/preferences.ts
Normal file
164
src/vs/workbench/services/preferences/common/preferences.ts
Normal 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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@@ -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 |
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user