mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-15 02:48:30 -05:00
Merge VS Code 1.31.1 (#4283)
This commit is contained in:
@@ -1,375 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { isFalsyOrWhitespace } from 'vs/base/common/strings';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { forEach } from 'vs/base/common/collections';
|
||||
import { IExtensionPointUser, ExtensionMessageCollector, ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { MenuId, MenuRegistry, ILocalizedString, IMenuItem } from 'vs/platform/actions/common/actions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
namespace schema {
|
||||
|
||||
// --- menus contribution point
|
||||
|
||||
export interface IUserFriendlyMenuItem {
|
||||
command: string;
|
||||
alt?: string;
|
||||
when?: string;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export function parseMenuId(value: string): MenuId | undefined {
|
||||
switch (value) {
|
||||
case 'commandPalette': return MenuId.CommandPalette;
|
||||
case 'touchBar': return MenuId.TouchBarContext;
|
||||
case 'editor/title': return MenuId.EditorTitle;
|
||||
case 'editor/context': return MenuId.EditorContext;
|
||||
case 'explorer/context': return MenuId.ExplorerContext;
|
||||
case 'editor/title/context': return MenuId.EditorTitleContext;
|
||||
case 'debug/callstack/context': return MenuId.DebugCallStackContext;
|
||||
case 'scm/title': return MenuId.SCMTitle;
|
||||
case 'scm/sourceControl': return MenuId.SCMSourceControl;
|
||||
case 'scm/resourceGroup/context': return MenuId.SCMResourceGroupContext;
|
||||
case 'scm/resourceState/context': return MenuId.SCMResourceContext;
|
||||
case 'scm/change/title': return MenuId.SCMChangeContext;
|
||||
case 'view/title': return MenuId.ViewTitle;
|
||||
case 'view/item/context': return MenuId.ViewItemContext;
|
||||
// {{SQL CARBON EDIT}}
|
||||
case 'objectExplorer/item/context': return MenuId.ObjectExplorerItemContext;
|
||||
case 'notebook/toolbar': return MenuId.NotebookToolbar;
|
||||
case 'dataExplorer/context': return MenuId.DataExplorerContext;
|
||||
}
|
||||
|
||||
return void 0;
|
||||
}
|
||||
|
||||
export function isValidMenuItems(menu: IUserFriendlyMenuItem[], collector: ExtensionMessageCollector): boolean {
|
||||
if (!Array.isArray(menu)) {
|
||||
collector.error(localize('requirearray', "menu items must be an array"));
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let item of menu) {
|
||||
if (typeof item.command !== 'string') {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
|
||||
return false;
|
||||
}
|
||||
if (item.alt && typeof item.alt !== 'string') {
|
||||
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'alt'));
|
||||
return false;
|
||||
}
|
||||
if (item.when && typeof item.when !== 'string') {
|
||||
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));
|
||||
return false;
|
||||
}
|
||||
if (item.group && typeof item.group !== 'string') {
|
||||
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'group'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const menuItem: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: {
|
||||
description: localize('vscode.extension.contributes.menuItem.command', 'Identifier of the command to execute. The command must be declared in the \'commands\'-section'),
|
||||
type: 'string'
|
||||
},
|
||||
alt: {
|
||||
description: localize('vscode.extension.contributes.menuItem.alt', 'Identifier of an alternative command to execute. The command must be declared in the \'commands\'-section'),
|
||||
type: 'string'
|
||||
},
|
||||
when: {
|
||||
description: localize('vscode.extension.contributes.menuItem.when', 'Condition which must be true to show this item'),
|
||||
type: 'string'
|
||||
},
|
||||
group: {
|
||||
description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this command belongs'),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const menusContribtion: IJSONSchema = {
|
||||
description: localize('vscode.extension.contributes.menus', "Contributes menu items to the editor"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'commandPalette': {
|
||||
description: localize('menus.commandPalette', "The Command Palette"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'touchBar': {
|
||||
description: localize('menus.touchBar', "The touch bar (macOS only)"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'editor/title': {
|
||||
description: localize('menus.editorTitle', "The editor title menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'editor/context': {
|
||||
description: localize('menus.editorContext', "The editor context menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'explorer/context': {
|
||||
description: localize('menus.explorerContext', "The file explorer context menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'editor/title/context': {
|
||||
description: localize('menus.editorTabContext', "The editor tabs context menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'debug/callstack/context': {
|
||||
description: localize('menus.debugCallstackContext', "The debug callstack context menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'scm/title': {
|
||||
description: localize('menus.scmTitle', "The Source Control title menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'scm/sourceControl': {
|
||||
description: localize('menus.scmSourceControl', "The Source Control menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'scm/resourceGroup/context': {
|
||||
description: localize('menus.resourceGroupContext', "The Source Control resource group context menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'scm/resourceState/context': {
|
||||
description: localize('menus.resourceStateContext', "The Source Control resource state context menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'view/title': {
|
||||
description: localize('view.viewTitle', "The contributed view title menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
},
|
||||
'view/item/context': {
|
||||
description: localize('view.itemContext', "The contributed view item context menu"),
|
||||
type: 'array',
|
||||
items: menuItem
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- commands contribution point
|
||||
|
||||
export interface IUserFriendlyCommand {
|
||||
command: string;
|
||||
title: string | ILocalizedString;
|
||||
category?: string | ILocalizedString;
|
||||
icon?: IUserFriendlyIcon;
|
||||
}
|
||||
|
||||
export type IUserFriendlyIcon = string | { light: string; dark: string; };
|
||||
|
||||
export function isValidCommand(command: IUserFriendlyCommand, collector: ExtensionMessageCollector): boolean {
|
||||
if (!command) {
|
||||
collector.error(localize('nonempty', "expected non-empty value."));
|
||||
return false;
|
||||
}
|
||||
if (isFalsyOrWhitespace(command.command)) {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'command'));
|
||||
return false;
|
||||
}
|
||||
if (!isValidLocalizedString(command.title, collector, 'title')) {
|
||||
return false;
|
||||
}
|
||||
if (command.category && !isValidLocalizedString(command.category, collector, 'category')) {
|
||||
return false;
|
||||
}
|
||||
if (!isValidIcon(command.icon, collector)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isValidIcon(icon: IUserFriendlyIcon | undefined, collector: ExtensionMessageCollector): boolean {
|
||||
if (typeof icon === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
if (typeof icon === 'string') {
|
||||
return true;
|
||||
} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {
|
||||
return true;
|
||||
}
|
||||
collector.error(localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));
|
||||
return false;
|
||||
}
|
||||
|
||||
function isValidLocalizedString(localized: string | ILocalizedString, collector: ExtensionMessageCollector, propertyName: string): boolean {
|
||||
if (typeof localized === 'undefined') {
|
||||
collector.error(localize('requireStringOrObject', "property `{0}` is mandatory and must be of type `string` or `object`", propertyName));
|
||||
return false;
|
||||
} else if (typeof localized === 'string' && isFalsyOrWhitespace(localized)) {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", propertyName));
|
||||
return false;
|
||||
} else if (typeof localized !== 'string' && (isFalsyOrWhitespace(localized.original) || isFalsyOrWhitespace(localized.value))) {
|
||||
collector.error(localize('requirestrings', "properties `{0}` and `{1}` are mandatory and must be of type `string`", `${propertyName}.value`, `${propertyName}.original`));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const commandType: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
command: {
|
||||
description: localize('vscode.extension.contributes.commandType.command', 'Identifier of the command to execute'),
|
||||
type: 'string'
|
||||
},
|
||||
title: {
|
||||
description: localize('vscode.extension.contributes.commandType.title', 'Title by which the command is represented in the UI'),
|
||||
type: 'string'
|
||||
},
|
||||
category: {
|
||||
description: localize('vscode.extension.contributes.commandType.category', '(Optional) Category string by the command is grouped in the UI'),
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
description: localize('vscode.extension.contributes.commandType.icon', '(Optional) Icon which is used to represent the command in the UI. Either a file path or a themable configuration'),
|
||||
anyOf: [{
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
light: {
|
||||
description: localize('vscode.extension.contributes.commandType.icon.light', 'Icon path when a light theme is used'),
|
||||
type: 'string'
|
||||
},
|
||||
dark: {
|
||||
description: localize('vscode.extension.contributes.commandType.icon.dark', 'Icon path when a dark theme is used'),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const commandsContribution: IJSONSchema = {
|
||||
description: localize('vscode.extension.contributes.commands', "Contributes commands to the command palette."),
|
||||
oneOf: [
|
||||
commandType,
|
||||
{
|
||||
type: 'array',
|
||||
items: commandType
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<schema.IUserFriendlyCommand | schema.IUserFriendlyCommand[]>('commands', [], schema.commandsContribution).setHandler(extensions => {
|
||||
|
||||
function handleCommand(userFriendlyCommand: schema.IUserFriendlyCommand, extension: IExtensionPointUser<any>) {
|
||||
|
||||
if (!schema.isValidCommand(userFriendlyCommand, extension.collector)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { icon, category, title, command } = userFriendlyCommand;
|
||||
|
||||
let absoluteIcon: { dark: URI; light?: URI; } | undefined;
|
||||
if (icon) {
|
||||
if (typeof icon === 'string') {
|
||||
absoluteIcon = { dark: resources.joinPath(extension.description.extensionLocation, icon) };
|
||||
} else {
|
||||
absoluteIcon = {
|
||||
dark: resources.joinPath(extension.description.extensionLocation, icon.dark),
|
||||
light: resources.joinPath(extension.description.extensionLocation, icon.light)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (MenuRegistry.addCommand({ id: command, title, category, iconLocation: absoluteIcon })) {
|
||||
extension.collector.info(localize('dup', "Command `{0}` appears multiple times in the `commands` section.", userFriendlyCommand.command));
|
||||
}
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
const { value } = extension;
|
||||
if (Array.isArray<schema.IUserFriendlyCommand>(value)) {
|
||||
for (let command of value) {
|
||||
handleCommand(command, extension);
|
||||
}
|
||||
} else {
|
||||
handleCommand(value, extension);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
ExtensionsRegistry.registerExtensionPoint<{ [loc: string]: schema.IUserFriendlyMenuItem[] }>('menus', [], schema.menusContribtion).setHandler(extensions => {
|
||||
for (let extension of extensions) {
|
||||
const { value, collector } = extension;
|
||||
|
||||
forEach(value, entry => {
|
||||
if (!schema.isValidMenuItems(entry.value, collector)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menu = schema.parseMenuId(entry.key);
|
||||
if (typeof menu !== 'number') {
|
||||
collector.warn(localize('menuId.invalid', "`{0}` is not a valid menu identifier", entry.key));
|
||||
return;
|
||||
}
|
||||
|
||||
for (let item of entry.value) {
|
||||
let command = MenuRegistry.getCommand(item.command);
|
||||
let alt = item.alt && MenuRegistry.getCommand(item.alt);
|
||||
|
||||
if (!command) {
|
||||
collector.error(localize('missing.command', "Menu item references a command `{0}` which is not defined in the 'commands' section.", item.command));
|
||||
continue;
|
||||
}
|
||||
if (item.alt && !alt) {
|
||||
collector.warn(localize('missing.altCommand', "Menu item references an alt-command `{0}` which is not defined in the 'commands' section.", item.alt));
|
||||
}
|
||||
if (item.command === item.alt) {
|
||||
collector.info(localize('dupe.command', "Menu item references the same command as default and alt-command"));
|
||||
}
|
||||
|
||||
let group: string | undefined;
|
||||
let order: number | undefined;
|
||||
if (item.group) {
|
||||
const idx = item.group.lastIndexOf('@');
|
||||
if (idx > 0) {
|
||||
group = item.group.substr(0, idx);
|
||||
order = Number(item.group.substr(idx + 1)) || undefined;
|
||||
} else {
|
||||
group = item.group;
|
||||
}
|
||||
}
|
||||
|
||||
MenuRegistry.appendMenuItem(menu, {
|
||||
command,
|
||||
alt,
|
||||
group,
|
||||
order,
|
||||
when: ContextKeyExpr.deserialize(item.when)
|
||||
} as IMenuItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -16,7 +16,7 @@ export class ActivityService implements IActivityService {
|
||||
constructor(
|
||||
private activitybarPart: ActivitybarPart,
|
||||
private panelPart: PanelPart,
|
||||
@IPanelService private panelService: IPanelService
|
||||
@IPanelService private readonly panelService: IPanelService
|
||||
) { }
|
||||
|
||||
showActivity(compositeOrActionId: string, badge: IBadge, clazz?: string, priority?: number): IDisposable {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { URI as Uri } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IResolveContentOptions, IUpdateContentOptions, ITextSnapshot } from 'vs/platform/files/common/files';
|
||||
import { ITextBufferFactory } from 'vs/editor/common/model';
|
||||
|
||||
@@ -23,7 +22,7 @@ export interface IBackupFileService {
|
||||
/**
|
||||
* Finds out if there are any backups stored.
|
||||
*/
|
||||
hasBackups(): TPromise<boolean>;
|
||||
hasBackups(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Loads the backup resource for a particular resource within the current workspace.
|
||||
@@ -31,7 +30,7 @@ export interface IBackupFileService {
|
||||
* @param resource The resource that is backed up.
|
||||
* @return The backup resource if any.
|
||||
*/
|
||||
loadBackupResource(resource: Uri): TPromise<Uri | undefined>;
|
||||
loadBackupResource(resource: Uri): Promise<Uri | undefined>;
|
||||
|
||||
/**
|
||||
* Given a resource, returns the associated backup resource.
|
||||
@@ -48,14 +47,14 @@ export interface IBackupFileService {
|
||||
* @param content The content of the resource as snapshot.
|
||||
* @param versionId The version id of the resource to backup.
|
||||
*/
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise<void>;
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets a list of file backups for the current workspace.
|
||||
*
|
||||
* @return The list of backups.
|
||||
*/
|
||||
getWorkspaceFileBackups(): TPromise<Uri[]>;
|
||||
getWorkspaceFileBackups(): Promise<Uri[]>;
|
||||
|
||||
/**
|
||||
* Resolves the backup for the given resource.
|
||||
@@ -63,18 +62,18 @@ export interface IBackupFileService {
|
||||
* @param value The contents from a backup resource as stream.
|
||||
* @return The backup file's backed up content as text buffer factory.
|
||||
*/
|
||||
resolveBackupContent(backup: Uri): TPromise<ITextBufferFactory | undefined>;
|
||||
resolveBackupContent(backup: Uri): Promise<ITextBufferFactory | undefined>;
|
||||
|
||||
/**
|
||||
* Discards the backup associated with a resource if it exists..
|
||||
*
|
||||
* @param resource The resource whose backup is being discarded discard to back up.
|
||||
*/
|
||||
discardResourceBackup(resource: Uri): TPromise<void>;
|
||||
discardResourceBackup(resource: Uri): Promise<void>;
|
||||
|
||||
/**
|
||||
* Discards all backups associated with the current workspace and prevents further backups from
|
||||
* being made.
|
||||
*/
|
||||
discardAllWorkspaceBackups(): TPromise<void>;
|
||||
discardAllWorkspaceBackups(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -10,14 +10,13 @@ import { URI as Uri } from 'vs/base/common/uri';
|
||||
import { ResourceQueue } from 'vs/base/common/async';
|
||||
import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS, BACKUP_FILE_RESOLVE_OPTIONS } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IFileService, ITextSnapshot } from 'vs/platform/files/common/files';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { readToMatchingString } from 'vs/base/node/stream';
|
||||
import { ITextBufferFactory } from 'vs/editor/common/model';
|
||||
import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
||||
import { keys } from 'vs/base/common/map';
|
||||
|
||||
export interface IBackupFilesModel {
|
||||
resolve(backupRoot: string): TPromise<IBackupFilesModel>;
|
||||
resolve(backupRoot: string): Promise<IBackupFilesModel>;
|
||||
|
||||
add(resource: Uri, versionId?: number): void;
|
||||
has(resource: Uri, versionId?: number): boolean;
|
||||
@@ -51,11 +50,11 @@ export class BackupSnapshot implements ITextSnapshot {
|
||||
export class BackupFilesModel implements IBackupFilesModel {
|
||||
private cache: { [resource: string]: number /* version ID */ } = Object.create(null);
|
||||
|
||||
resolve(backupRoot: string): TPromise<IBackupFilesModel> {
|
||||
resolve(backupRoot: string): Promise<IBackupFilesModel> {
|
||||
return pfs.readDirsInDir(backupRoot).then(backupSchemas => {
|
||||
|
||||
// For all supported schemas
|
||||
return TPromise.join(backupSchemas.map(backupSchema => {
|
||||
return Promise.all(backupSchemas.map(backupSchema => {
|
||||
|
||||
// Read backup directory for backups
|
||||
const backupSchemaPath = path.join(backupRoot, backupSchema);
|
||||
@@ -114,12 +113,12 @@ export class BackupFileService implements IBackupFileService {
|
||||
private backupWorkspacePath: string;
|
||||
|
||||
private isShuttingDown: boolean;
|
||||
private ready: TPromise<IBackupFilesModel>;
|
||||
private ready: Promise<IBackupFilesModel>;
|
||||
private ioOperationQueues: ResourceQueue; // queue IO operations to ensure write order
|
||||
|
||||
constructor(
|
||||
backupWorkspacePath: string,
|
||||
@IFileService private fileService: IFileService
|
||||
@IFileService private readonly fileService: IFileService
|
||||
) {
|
||||
this.isShuttingDown = false;
|
||||
this.ioOperationQueues = new ResourceQueue();
|
||||
@@ -133,19 +132,19 @@ export class BackupFileService implements IBackupFileService {
|
||||
this.ready = this.init();
|
||||
}
|
||||
|
||||
private init(): TPromise<IBackupFilesModel> {
|
||||
private init(): Promise<IBackupFilesModel> {
|
||||
const model = new BackupFilesModel();
|
||||
|
||||
return model.resolve(this.backupWorkspacePath);
|
||||
}
|
||||
|
||||
hasBackups(): TPromise<boolean> {
|
||||
hasBackups(): Promise<boolean> {
|
||||
return this.ready.then(model => {
|
||||
return model.count() > 0;
|
||||
});
|
||||
}
|
||||
|
||||
loadBackupResource(resource: Uri): TPromise<Uri | undefined> {
|
||||
loadBackupResource(resource: Uri): Promise<Uri | undefined> {
|
||||
return this.ready.then(model => {
|
||||
|
||||
// Return directly if we have a known backup with that resource
|
||||
@@ -154,19 +153,19 @@ export class BackupFileService implements IBackupFileService {
|
||||
return backupResource;
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise<void> {
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void> {
|
||||
if (this.isShuttingDown) {
|
||||
return TPromise.as(void 0);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.ready.then(model => {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (model.has(backupResource, versionId)) {
|
||||
return void 0; // return early if backup version id matches requested one
|
||||
return undefined; // return early if backup version id matches requested one
|
||||
}
|
||||
|
||||
return this.ioOperationQueues.queueFor(backupResource).queue(() => {
|
||||
@@ -178,7 +177,7 @@ export class BackupFileService implements IBackupFileService {
|
||||
});
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: Uri): TPromise<void> {
|
||||
discardResourceBackup(resource: Uri): Promise<void> {
|
||||
return this.ready.then(model => {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
@@ -188,7 +187,7 @@ export class BackupFileService implements IBackupFileService {
|
||||
});
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): TPromise<void> {
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
return this.ready.then(model => {
|
||||
@@ -196,9 +195,9 @@ export class BackupFileService implements IBackupFileService {
|
||||
});
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): TPromise<Uri[]> {
|
||||
getWorkspaceFileBackups(): Promise<Uri[]> {
|
||||
return this.ready.then(model => {
|
||||
const readPromises: TPromise<Uri>[] = [];
|
||||
const readPromises: Promise<Uri>[] = [];
|
||||
|
||||
model.get().forEach(fileBackup => {
|
||||
readPromises.push(
|
||||
@@ -206,11 +205,11 @@ export class BackupFileService implements IBackupFileService {
|
||||
);
|
||||
});
|
||||
|
||||
return TPromise.join(readPromises);
|
||||
return Promise.all(readPromises);
|
||||
});
|
||||
}
|
||||
|
||||
resolveBackupContent(backup: Uri): TPromise<ITextBufferFactory> {
|
||||
resolveBackupContent(backup: Uri): Promise<ITextBufferFactory> {
|
||||
return this.fileService.resolveStreamContent(backup, BACKUP_FILE_RESOLVE_OPTIONS).then(content => {
|
||||
|
||||
// Add a filter method to filter out everything until the meta marker
|
||||
@@ -248,49 +247,49 @@ export class InMemoryBackupFileService implements IBackupFileService {
|
||||
|
||||
private backups: Map<string, ITextSnapshot> = new Map();
|
||||
|
||||
hasBackups(): TPromise<boolean> {
|
||||
return TPromise.as(this.backups.size > 0);
|
||||
hasBackups(): Promise<boolean> {
|
||||
return Promise.resolve(this.backups.size > 0);
|
||||
}
|
||||
|
||||
loadBackupResource(resource: Uri): TPromise<Uri | undefined> {
|
||||
loadBackupResource(resource: Uri): Promise<Uri | undefined> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
if (this.backups.has(backupResource.toString())) {
|
||||
return TPromise.as(backupResource);
|
||||
return Promise.resolve(backupResource);
|
||||
}
|
||||
|
||||
return TPromise.as(void 0);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): TPromise<void> {
|
||||
backupResource(resource: Uri, content: ITextSnapshot, versionId?: number): Promise<void> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
this.backups.set(backupResource.toString(), content);
|
||||
|
||||
return TPromise.as(void 0);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
resolveBackupContent(backupResource: Uri): TPromise<ITextBufferFactory | undefined> {
|
||||
resolveBackupContent(backupResource: Uri): Promise<ITextBufferFactory | undefined> {
|
||||
const snapshot = this.backups.get(backupResource.toString());
|
||||
if (snapshot) {
|
||||
return TPromise.as(createTextBufferFactoryFromSnapshot(snapshot));
|
||||
return Promise.resolve(createTextBufferFactoryFromSnapshot(snapshot));
|
||||
}
|
||||
|
||||
return TPromise.as(void 0);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getWorkspaceFileBackups(): TPromise<Uri[]> {
|
||||
return TPromise.as(keys(this.backups).map(key => Uri.parse(key)));
|
||||
getWorkspaceFileBackups(): Promise<Uri[]> {
|
||||
return Promise.resolve(keys(this.backups).map(key => Uri.parse(key)));
|
||||
}
|
||||
|
||||
discardResourceBackup(resource: Uri): TPromise<void> {
|
||||
discardResourceBackup(resource: Uri): Promise<void> {
|
||||
this.backups.delete(this.toBackupResource(resource).toString());
|
||||
|
||||
return TPromise.as(void 0);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
discardAllWorkspaceBackups(): TPromise<void> {
|
||||
discardAllWorkspaceBackups(): Promise<void> {
|
||||
this.backups.clear();
|
||||
|
||||
return TPromise.as(void 0);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
toBackupResource(resource: Uri): Uri {
|
||||
|
||||
@@ -92,7 +92,7 @@ suite('BackupFileService', () => {
|
||||
service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath);
|
||||
return service.loadBackupResource(fooFile).then(resource => {
|
||||
assert.ok(resource);
|
||||
assert.equal(path.basename(resource.fsPath), path.basename(fooBackupPath));
|
||||
assert.equal(path.basename(resource!.fsPath), path.basename(fooBackupPath));
|
||||
return service.hasBackups().then(hasBackups => {
|
||||
assert.ok(hasBackups);
|
||||
});
|
||||
|
||||
@@ -69,16 +69,23 @@ class ModelEditTask implements IDisposable {
|
||||
// honor eol-change
|
||||
this._newEol = edit.eol;
|
||||
}
|
||||
if (edit.range || edit.text) {
|
||||
// create edit operation
|
||||
let range: Range;
|
||||
if (!edit.range) {
|
||||
range = this._model.getFullModelRange();
|
||||
} else {
|
||||
range = Range.lift(edit.range);
|
||||
}
|
||||
this._edits.push(EditOperation.replaceMove(range, edit.text));
|
||||
if (!edit.range && !edit.text) {
|
||||
// lacks both a range and the text
|
||||
continue;
|
||||
}
|
||||
if (Range.isEmpty(edit.range) && !edit.text) {
|
||||
// no-op edit (replace empty range with empty text)
|
||||
continue;
|
||||
}
|
||||
|
||||
// create edit operation
|
||||
let range: Range;
|
||||
if (!edit.range) {
|
||||
range = this._model.getFullModelRange();
|
||||
} else {
|
||||
range = Range.lift(edit.range);
|
||||
}
|
||||
this._edits.push(EditOperation.replaceMove(range, edit.text));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,9 +128,11 @@ class EditorEditTask extends ModelEditTask {
|
||||
this._editor.pushUndoStop();
|
||||
}
|
||||
if (this._newEol !== undefined) {
|
||||
this._editor.pushUndoStop();
|
||||
this._editor.getModel().pushEOL(this._newEol);
|
||||
this._editor.pushUndoStop();
|
||||
if (this._editor.hasModel()) {
|
||||
this._editor.pushUndoStop();
|
||||
this._editor.getModel().pushEOL(this._newEol);
|
||||
this._editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,13 +141,13 @@ class BulkEditModel implements IDisposable {
|
||||
|
||||
private _textModelResolverService: ITextModelService;
|
||||
private _edits = new Map<string, ResourceTextEdit[]>();
|
||||
private _editor: ICodeEditor;
|
||||
private _editor: ICodeEditor | undefined;
|
||||
private _tasks: ModelEditTask[];
|
||||
private _progress: IProgress<void>;
|
||||
|
||||
constructor(
|
||||
textModelResolverService: ITextModelService,
|
||||
editor: ICodeEditor,
|
||||
editor: ICodeEditor | undefined,
|
||||
edits: ResourceTextEdit[],
|
||||
progress: IProgress<void>
|
||||
) {
|
||||
@@ -169,7 +178,7 @@ class BulkEditModel implements IDisposable {
|
||||
}
|
||||
|
||||
this._tasks = [];
|
||||
const promises: Thenable<any>[] = [];
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
this._edits.forEach((value, key) => {
|
||||
const promise = this._textModelResolverService.createModelReference(URI.parse(key)).then(ref => {
|
||||
@@ -180,7 +189,7 @@ class BulkEditModel implements IDisposable {
|
||||
}
|
||||
|
||||
let task: ModelEditTask;
|
||||
if (this._editor && this._editor.getModel().uri.toString() === model.textEditorModel.uri.toString()) {
|
||||
if (this._editor && this._editor.hasModel() && this._editor.getModel().uri.toString() === model.textEditorModel.uri.toString()) {
|
||||
task = new EditorEditTask(ref, this._editor);
|
||||
} else {
|
||||
task = new ModelEditTask(ref);
|
||||
@@ -221,12 +230,12 @@ export type Edit = ResourceFileEdit | ResourceTextEdit;
|
||||
export class BulkEdit {
|
||||
|
||||
private _edits: Edit[] = [];
|
||||
private _editor: ICodeEditor;
|
||||
private _progress: IProgressRunner;
|
||||
private _editor: ICodeEditor | undefined;
|
||||
private _progress?: IProgressRunner;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
progress: IProgressRunner,
|
||||
editor: ICodeEditor | undefined,
|
||||
progress: IProgressRunner | undefined,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@@ -264,7 +273,7 @@ export class BulkEdit {
|
||||
let total = 0;
|
||||
|
||||
const groups: Edit[][] = [];
|
||||
let group: Edit[];
|
||||
let group: Edit[] | undefined;
|
||||
for (const edit of this._edits) {
|
||||
if (!group
|
||||
|| (isResourceFileEdit(group[0]) && !isResourceFileEdit(edit))
|
||||
@@ -285,8 +294,10 @@ export class BulkEdit {
|
||||
|
||||
// define total work and progress callback
|
||||
// for child operations
|
||||
this._progress.total(total);
|
||||
let progress: IProgress<void> = { report: _ => this._progress.worked(1) };
|
||||
if (this._progress) {
|
||||
this._progress.total(total);
|
||||
}
|
||||
let progress: IProgress<void> = { report: _ => this._progress && this._progress.worked(1) };
|
||||
|
||||
// do it.
|
||||
for (const group of groups) {
|
||||
@@ -380,8 +391,7 @@ export class BulkEditService implements IBulkEditService {
|
||||
let codeEditor = options.editor;
|
||||
|
||||
// First check if loaded models were not changed in the meantime
|
||||
for (let i = 0, len = edits.length; i < len; i++) {
|
||||
const edit = edits[i];
|
||||
for (const edit of edits) {
|
||||
if (!isResourceFileEdit(edit) && typeof edit.modelVersionId === 'number') {
|
||||
let model = this._modelService.getModel(edit.resource);
|
||||
if (model && model.getVersionId() !== edit.modelVersionId) {
|
||||
@@ -400,7 +410,7 @@ export class BulkEditService implements IBulkEditService {
|
||||
}
|
||||
}
|
||||
|
||||
const bulkEdit = new BulkEdit(options.editor, options.progress, this._logService, this._textModelService, this._fileService, this._textFileService, this._labelService, this._configurationService);
|
||||
const bulkEdit = new BulkEdit(codeEditor, options.progress, this._logService, this._textModelService, this._fileService, this._textFileService, this._labelService, this._configurationService);
|
||||
bulkEdit.add(edits);
|
||||
|
||||
return bulkEdit.perform().then(() => {
|
||||
|
||||
@@ -14,13 +14,13 @@ import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/
|
||||
export class CodeEditorService extends CodeEditorServiceImpl {
|
||||
|
||||
constructor(
|
||||
@IEditorService private editorService: IEditorService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
super(themeService);
|
||||
}
|
||||
|
||||
getActiveCodeEditor(): ICodeEditor {
|
||||
getActiveCodeEditor(): ICodeEditor | null {
|
||||
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
|
||||
if (isCodeEditor(activeTextEditorWidget)) {
|
||||
return activeTextEditorWidget;
|
||||
@@ -33,7 +33,7 @@ export class CodeEditorService extends CodeEditorServiceImpl {
|
||||
return null;
|
||||
}
|
||||
|
||||
openCodeEditor(input: IResourceInput, source: ICodeEditor | null, sideBySide?: boolean): Thenable<ICodeEditor> {
|
||||
openCodeEditor(input: IResourceInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
|
||||
|
||||
// Special case: If the active editor is a diff editor and the request to open originates and
|
||||
// targets the modified side of it, we just apply the request there to prevent opening the modified
|
||||
@@ -46,7 +46,7 @@ export class CodeEditorService extends CodeEditorServiceImpl {
|
||||
input.resource && // we need a request resource to compare with
|
||||
activeTextEditorWidget.getModel() && // we need a target model to compare with
|
||||
source === activeTextEditorWidget.getModifiedEditor() && // we need the source of this request to be the modified side of the diff editor
|
||||
input.resource.toString() === activeTextEditorWidget.getModel().modified.uri.toString() // we need the input resources to match with modified side
|
||||
input.resource.toString() === activeTextEditorWidget.getModel()!.modified.uri.toString() // we need the input resources to match with modified side
|
||||
) {
|
||||
const targetEditor = activeTextEditorWidget.getModifiedEditor();
|
||||
|
||||
@@ -60,7 +60,7 @@ export class CodeEditorService extends CodeEditorServiceImpl {
|
||||
return this.doOpenCodeEditor(input, source, sideBySide);
|
||||
}
|
||||
|
||||
private doOpenCodeEditor(input: IResourceInput, source: ICodeEditor, sideBySide?: boolean): Thenable<ICodeEditor> {
|
||||
private doOpenCodeEditor(input: IResourceInput, source: ICodeEditor | null, sideBySide?: boolean): Promise<ICodeEditor | null> {
|
||||
return this.editorService.openEditor(input, sideBySide ? SIDE_GROUP : ACTIVE_GROUP).then(control => {
|
||||
if (control) {
|
||||
const widget = control.getControl();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
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, filterEvent, toPromise } 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';
|
||||
|
||||
@@ -35,7 +35,7 @@ export class CommandService extends Disposable implements ICommandService {
|
||||
// we don't wait for it when the extension
|
||||
// host didn't yet start and the command is already registered
|
||||
|
||||
const activation: Thenable<any> = this._extensionService.activateByEvent(`onCommand:${id}`);
|
||||
const activation: Promise<any> = this._extensionService.activateByEvent(`onCommand:${id}`);
|
||||
const commandIsRegistered = !!CommandsRegistry.getCommand(id);
|
||||
|
||||
if (!this._extensionHostIsReady && commandIsRegistered) {
|
||||
@@ -46,7 +46,7 @@ export class CommandService extends Disposable implements ICommandService {
|
||||
waitFor = Promise.race<any>([
|
||||
// race activation events against command registration
|
||||
Promise.all([activation, this._extensionService.activateByEvent(`*`)]),
|
||||
toPromise(filterEvent(CommandsRegistry.onDidRegisterCommand, e => e === id)),
|
||||
Event.toPromise(Event.filter(CommandsRegistry.onDidRegisterCommand, e => e === id)),
|
||||
]);
|
||||
}
|
||||
return (waitFor as Promise<any>).then(_ => this._tryExecuteCommand(id, args));
|
||||
@@ -60,7 +60,7 @@ export class CommandService extends Disposable implements ICommandService {
|
||||
}
|
||||
try {
|
||||
this._onWillExecuteCommand.fire({ commandId: id });
|
||||
const result = this._instantiationService.invokeFunction.apply(this._instantiationService, [command.handler].concat(args));
|
||||
const result = this._instantiationService.invokeFunction.apply(this._instantiationService, [command.handler, ...args]);
|
||||
return Promise.resolve(result);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
|
||||
@@ -18,9 +18,10 @@ class SimpleExtensionService implements IExtensionService {
|
||||
get onDidRegisterExtensions(): Event<void> {
|
||||
return this._onDidRegisterExtensions.event;
|
||||
}
|
||||
onDidChangeExtensionsStatus = null;
|
||||
onWillActivateByEvent = null;
|
||||
onDidChangeResponsiveChange = null;
|
||||
onDidChangeExtensionsStatus = null!;
|
||||
onDidChangeExtensions = null!;
|
||||
onWillActivateByEvent = null!;
|
||||
onDidChangeResponsiveChange = null!;
|
||||
activateByEvent(activationEvent: string): Promise<void> {
|
||||
return this.whenInstalledExtensionsRegistered().then(() => { });
|
||||
}
|
||||
@@ -31,7 +32,7 @@ class SimpleExtensionService implements IExtensionService {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
getExtensionsStatus() {
|
||||
return undefined;
|
||||
return undefined!;
|
||||
}
|
||||
getExtensions(): Promise<IExtensionDescription[]> {
|
||||
return Promise.resolve([]);
|
||||
@@ -54,6 +55,8 @@ class SimpleExtensionService implements IExtensionService {
|
||||
}
|
||||
stopExtensionHost(): void {
|
||||
}
|
||||
canAddExtension(): boolean { return false; }
|
||||
canRemoveExtension(): boolean { return false; }
|
||||
}
|
||||
|
||||
suite('CommandService', function () {
|
||||
@@ -138,7 +141,7 @@ suite('CommandService', function () {
|
||||
assert.equal(callCounter, 0);
|
||||
|
||||
let reg = CommandsRegistry.registerCommand('bar', () => callCounter += 1);
|
||||
resolveFunc(true);
|
||||
resolveFunc!(true);
|
||||
|
||||
return r.then(() => {
|
||||
reg.dispose();
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as objects from 'vs/base/common/objects';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { IConfigurationNode, IConfigurationRegistry, Extensions, editorConfigurationSchemaId, IDefaultConfigurationExtension, validateProperty, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { workspaceSettingsSchemaId, launchSchemaId } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { isObject } from 'vs/base/common/types';
|
||||
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(Extensions.Configuration);
|
||||
|
||||
const configurationEntrySchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { title: '', properties: {} } }],
|
||||
properties: {
|
||||
title: {
|
||||
description: nls.localize('vscode.extension.contributes.configuration.title', 'A summary of the settings. This label will be used in the settings file as separating comment.'),
|
||||
type: 'string'
|
||||
},
|
||||
properties: {
|
||||
description: nls.localize('vscode.extension.contributes.configuration.properties', 'Description of the configuration properties.'),
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
anyOf: [
|
||||
{ $ref: 'http://json-schema.org/draft-07/schema#' },
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
isExecutable: {
|
||||
type: 'boolean',
|
||||
deprecationMessage: 'This property is deprecated. Instead use `scope` property and set it to `application` value.'
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
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.")
|
||||
],
|
||||
description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `window` and `resource`.")
|
||||
},
|
||||
enumDescriptions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description: nls.localize('scope.enumDescriptions', 'Descriptions for enum values')
|
||||
},
|
||||
markdownEnumDescription: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
description: nls.localize('scope.markdownEnumDescription', 'Descriptions for enum values in the markdown format.')
|
||||
},
|
||||
markdownDescription: {
|
||||
type: 'string',
|
||||
description: nls.localize('scope.markdownDescription', 'The description in the markdown format.')
|
||||
},
|
||||
deprecationMessage: {
|
||||
type: 'string',
|
||||
description: nls.localize('scope.deprecationMessage', 'If set, the property is marked as deprecated and the given message is shown as an explanation.')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let registeredDefaultConfigurations: IDefaultConfigurationExtension[] = [];
|
||||
|
||||
// BEGIN VSCode extension point `configurationDefaults`
|
||||
const defaultConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>('configurationDefaults', [], {
|
||||
description: nls.localize('vscode.extension.contributes.defaultConfiguration', 'Contributes default editor configuration settings by language.'),
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: {} }],
|
||||
patternProperties: {
|
||||
'\\[.*\\]$': {
|
||||
type: 'object',
|
||||
default: {},
|
||||
$ref: editorConfigurationSchemaId,
|
||||
}
|
||||
}
|
||||
});
|
||||
defaultConfigurationExtPoint.setHandler(extensions => {
|
||||
registeredDefaultConfigurations = extensions.map(extension => {
|
||||
const id = extension.description.id;
|
||||
const name = extension.description.name;
|
||||
const defaults = objects.deepClone(extension.value);
|
||||
return <IDefaultConfigurationExtension>{
|
||||
id, name, defaults
|
||||
};
|
||||
});
|
||||
});
|
||||
// END VSCode extension point `configurationDefaults`
|
||||
|
||||
|
||||
// BEGIN VSCode extension point `configuration`
|
||||
const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IConfigurationNode>('configuration', [defaultConfigurationExtPoint], {
|
||||
description: nls.localize('vscode.extension.contributes.configuration', 'Contributes configuration settings.'),
|
||||
oneOf: [
|
||||
configurationEntrySchema,
|
||||
{
|
||||
type: 'array',
|
||||
items: configurationEntrySchema
|
||||
}
|
||||
]
|
||||
});
|
||||
configurationExtPoint.setHandler(extensions => {
|
||||
const configurations: IConfigurationNode[] = [];
|
||||
|
||||
function handleConfiguration(node: IConfigurationNode, extension: IExtensionPointUser<any>) {
|
||||
let configuration = objects.deepClone(node);
|
||||
|
||||
if (configuration.title && (typeof configuration.title !== 'string')) {
|
||||
extension.collector.error(nls.localize('invalid.title', "'configuration.title' must be a string"));
|
||||
}
|
||||
|
||||
validateProperties(configuration, extension);
|
||||
|
||||
configuration.id = node.id || extension.description.id || extension.description.uuid;
|
||||
configuration.contributedByExtension = true;
|
||||
configuration.title = configuration.title || extension.description.displayName || extension.description.id;
|
||||
configurations.push(configuration);
|
||||
}
|
||||
|
||||
for (let extension of extensions) {
|
||||
const value = <IConfigurationNode | IConfigurationNode[]>extension.value;
|
||||
if (!Array.isArray(value)) {
|
||||
handleConfiguration(value, extension);
|
||||
} else {
|
||||
value.forEach(v => handleConfiguration(v, extension));
|
||||
}
|
||||
}
|
||||
configurationRegistry.registerConfigurations(configurations, registeredDefaultConfigurations, false);
|
||||
});
|
||||
// END VSCode extension point `configuration`
|
||||
|
||||
function validateProperties(configuration: IConfigurationNode, extension: IExtensionPointUser<any>): void {
|
||||
let properties = configuration.properties;
|
||||
if (properties) {
|
||||
if (typeof properties !== 'object') {
|
||||
extension.collector.error(nls.localize('invalid.properties', "'configuration.properties' must be an object"));
|
||||
configuration.properties = {};
|
||||
}
|
||||
for (let key in properties) {
|
||||
const message = validateProperty(key);
|
||||
if (message) {
|
||||
delete properties[key];
|
||||
extension.collector.warn(message);
|
||||
continue;
|
||||
}
|
||||
const propertyConfiguration = properties[key];
|
||||
if (!isObject(propertyConfiguration)) {
|
||||
delete properties[key];
|
||||
extension.collector.error(nls.localize('invalid.property', "'configuration.property' must be an object"));
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
let subNodes = configuration.allOf;
|
||||
if (subNodes) {
|
||||
extension.collector.error(nls.localize('invalid.allOf', "'configuration.allOf' is deprecated and should no longer be used. Instead, pass multiple configuration sections as an array to the 'configuration' contribution point."));
|
||||
for (let node of subNodes) {
|
||||
validateProperties(node, extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
|
||||
jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', {
|
||||
allowComments: true,
|
||||
default: {
|
||||
folders: [
|
||||
{
|
||||
path: ''
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
}
|
||||
},
|
||||
required: ['folders'],
|
||||
properties: {
|
||||
'folders': {
|
||||
minItems: 0,
|
||||
uniqueItems: true,
|
||||
description: nls.localize('workspaceConfig.folders.description', "List of folders to be loaded in the workspace."),
|
||||
items: {
|
||||
type: 'object',
|
||||
default: { path: '' },
|
||||
oneOf: [{
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: nls.localize('workspaceConfig.path.description', "A file path. e.g. `/root/folderA` or `./folderA` for a relative path that will be resolved against the location of the workspace file.")
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")
|
||||
}
|
||||
},
|
||||
required: ['path']
|
||||
}, {
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: nls.localize('workspaceConfig.uri.description', "URI of the folder")
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: nls.localize('workspaceConfig.name.description', "An optional name for the folder. ")
|
||||
}
|
||||
},
|
||||
required: ['uri']
|
||||
}]
|
||||
}
|
||||
},
|
||||
'settings': {
|
||||
type: 'object',
|
||||
default: {},
|
||||
description: nls.localize('workspaceConfig.settings.description', "Workspace settings"),
|
||||
$ref: workspaceSettingsSchemaId
|
||||
},
|
||||
'launch': {
|
||||
type: 'object',
|
||||
default: { configurations: [], compounds: [] },
|
||||
description: nls.localize('workspaceConfig.launch.description', "Workspace launch configurations"),
|
||||
$ref: launchSchemaId
|
||||
},
|
||||
'extensions': {
|
||||
type: 'object',
|
||||
default: {},
|
||||
description: nls.localize('workspaceConfig.extensions.description', "Workspace extensions"),
|
||||
$ref: 'vscode://schemas/extensions'
|
||||
}
|
||||
},
|
||||
additionalProperties: false,
|
||||
errorMessage: nls.localize('unknownWorkspaceProperty', "Unknown workspace configuration property")
|
||||
});
|
||||
@@ -202,7 +202,11 @@ export class Configuration extends BaseConfiguration {
|
||||
// Do not remove workspace configuration
|
||||
return new ConfigurationChangeEvent();
|
||||
}
|
||||
const keys = this.folders.get(folder).keys;
|
||||
const folderConfig = this.folders.get(folder);
|
||||
if (!folderConfig) {
|
||||
throw new Error('Unknown folder');
|
||||
}
|
||||
const keys = folderConfig.keys;
|
||||
super.deleteFolderConfiguration(folder);
|
||||
return new ConfigurationChangeEvent().change(keys, folder);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,10 @@ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { RunOnceScheduler, Delayer } from 'vs/base/common/async';
|
||||
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 } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { WorkspaceConfigurationModelParser, FolderSettingsModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels';
|
||||
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 { 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, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
@@ -30,96 +29,287 @@ import { Schemas } from 'vs/base/common/network';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationModel } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export interface IWorkspaceIdentifier {
|
||||
id: string;
|
||||
configPath: URI;
|
||||
}
|
||||
|
||||
export class WorkspaceConfiguration extends Disposable {
|
||||
|
||||
private _workspaceConfigPath: URI;
|
||||
private _workspaceConfigurationWatcher: ConfigWatcher<WorkspaceConfigurationModelParser>;
|
||||
private _workspaceConfigurationWatcherDisposables: IDisposable[] = [];
|
||||
private readonly _cachedConfiguration: CachedWorkspaceConfiguration;
|
||||
private _workspaceConfiguration: IWorkspaceConfiguration | null;
|
||||
private _workspaceIdentifier: IWorkspaceIdentifier | null = null;
|
||||
private _fileService: IFileService | null = null;
|
||||
|
||||
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 : '');
|
||||
private _cache: ConfigurationModel = new ConfigurationModel();
|
||||
constructor(
|
||||
environmentService: IEnvironmentService
|
||||
) {
|
||||
super();
|
||||
this._cachedConfiguration = new CachedWorkspaceConfiguration(environmentService);
|
||||
this._workspaceConfiguration = this._cachedConfiguration;
|
||||
}
|
||||
|
||||
load(workspaceConfigPath: URI): Promise<void> {
|
||||
if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) {
|
||||
return this.reload();
|
||||
}
|
||||
|
||||
this._workspaceConfigPath = workspaceConfigPath;
|
||||
|
||||
return new Promise<void>((c, e) => {
|
||||
const defaultConfig = new WorkspaceConfigurationModelParser(this._workspaceConfigPath.fsPath);
|
||||
defaultConfig.parse(JSON.stringify({ folders: [] } as IStoredWorkspace, null, '\t'));
|
||||
if (this._workspaceConfigurationWatcher) {
|
||||
this.disposeConfigurationWatcher();
|
||||
adopt(fileService: IFileService): Promise<boolean> {
|
||||
if (!this._fileService) {
|
||||
this._fileService = fileService;
|
||||
if (this.adoptWorkspaceConfiguration()) {
|
||||
if (this._workspaceIdentifier) {
|
||||
return this._workspaceConfiguration.load(this._workspaceIdentifier).then(() => true);
|
||||
}
|
||||
}
|
||||
this._workspaceConfigurationWatcher = new ConfigWatcher(this._workspaceConfigPath.fsPath, {
|
||||
changeBufferDelay: 300,
|
||||
onError: error => errors.onUnexpectedError(error),
|
||||
defaultConfig,
|
||||
parse: (content: string, parseErrors: any[]) => {
|
||||
this._workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(this._workspaceConfigPath.fsPath);
|
||||
this._workspaceConfigurationModelParser.parse(content);
|
||||
parseErrors = [...this._workspaceConfigurationModelParser.errors];
|
||||
this.consolidate();
|
||||
return this._workspaceConfigurationModelParser;
|
||||
}, initCallback: () => c(null)
|
||||
});
|
||||
this.listenToWatcher();
|
||||
});
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
|
||||
this._workspaceIdentifier = workspaceIdentifier;
|
||||
this.adoptWorkspaceConfiguration();
|
||||
return this._workspaceConfiguration.load(this._workspaceIdentifier);
|
||||
}
|
||||
|
||||
reload(): Promise<void> {
|
||||
this.stopListeningToWatcher();
|
||||
return new Promise<void>(c => this._workspaceConfigurationWatcher.reload(() => {
|
||||
this.listenToWatcher();
|
||||
c(null);
|
||||
}));
|
||||
return this._workspaceIdentifier ? this.load(this._workspaceIdentifier) : Promise.resolve();
|
||||
}
|
||||
|
||||
getFolders(): IStoredWorkspaceFolder[] {
|
||||
return this._workspaceConfiguration.getFolders();
|
||||
}
|
||||
|
||||
setFolders(folders: IStoredWorkspaceFolder[], jsonEditingService: JSONEditingService): Promise<void> {
|
||||
if (this._workspaceIdentifier) {
|
||||
return jsonEditingService.write(this._workspaceIdentifier.configPath, { key: 'folders', value: folders }, true)
|
||||
.then(() => this.reload());
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getConfiguration(): ConfigurationModel {
|
||||
return this._workspaceConfiguration.getWorkspaceSettings();
|
||||
}
|
||||
|
||||
reprocessWorkspaceSettings(): ConfigurationModel {
|
||||
this._workspaceConfiguration.reprocessWorkspaceSettings();
|
||||
return this.getConfiguration();
|
||||
}
|
||||
|
||||
private adoptWorkspaceConfiguration(): boolean {
|
||||
if (this._workspaceIdentifier) {
|
||||
if (this._fileService) {
|
||||
if (!(this._workspaceConfiguration instanceof FileServiceBasedWorkspaceConfiguration)) {
|
||||
dispose(this._workspaceConfiguration);
|
||||
const nodeBasedWorkspaceConfiguration = this._workspaceConfiguration instanceof NodeBasedWorkspaceConfiguration ? this._workspaceConfiguration : undefined;
|
||||
this._workspaceConfiguration = new FileServiceBasedWorkspaceConfiguration(this._fileService, nodeBasedWorkspaceConfiguration);
|
||||
this._register(this._workspaceConfiguration.onDidChange(e => this.onDidWorkspaceConfigurationChange()));
|
||||
return !nodeBasedWorkspaceConfiguration;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (this._workspaceIdentifier.configPath.scheme === Schemas.file) {
|
||||
if (!(this._workspaceConfiguration instanceof NodeBasedWorkspaceConfiguration)) {
|
||||
dispose(this._workspaceConfiguration);
|
||||
this._workspaceConfiguration = new NodeBasedWorkspaceConfiguration();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private onDidWorkspaceConfigurationChange(): void {
|
||||
this.updateCache();
|
||||
this.reload().then(() => this._onDidUpdateConfiguration.fire());
|
||||
}
|
||||
|
||||
private updateCache(): Promise<void> {
|
||||
if (this._workspaceIdentifier && this._workspaceIdentifier.configPath.scheme !== Schemas.file && this._workspaceConfiguration instanceof FileServiceBasedWorkspaceConfiguration) {
|
||||
return this._workspaceConfiguration.load(this._workspaceIdentifier)
|
||||
.then(() => this._cachedConfiguration.updateWorkspace(this._workspaceIdentifier, this._workspaceConfiguration.getConfigurationModel()));
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
interface IWorkspaceConfiguration extends IDisposable {
|
||||
readonly onDidChange: Event<void>;
|
||||
load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void>;
|
||||
getConfigurationModel(): ConfigurationModel;
|
||||
getFolders(): IStoredWorkspaceFolder[];
|
||||
getWorkspaceSettings(): ConfigurationModel;
|
||||
reprocessWorkspaceSettings(): ConfigurationModel;
|
||||
}
|
||||
|
||||
abstract class AbstractWorkspaceConfiguration extends Disposable implements IWorkspaceConfiguration {
|
||||
|
||||
private _workspaceConfigurationModelParser: WorkspaceConfigurationModelParser;
|
||||
private _workspaceSettings: ConfigurationModel;
|
||||
private _workspaceIdentifier: IWorkspaceIdentifier | null = null;
|
||||
|
||||
protected readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
constructor(from?: AbstractWorkspaceConfiguration) {
|
||||
super();
|
||||
|
||||
this._workspaceConfigurationModelParser = from ? from._workspaceConfigurationModelParser : new WorkspaceConfigurationModelParser('');
|
||||
this._workspaceSettings = new ConfigurationModel();
|
||||
}
|
||||
|
||||
get workspaceIdentifier(): IWorkspaceIdentifier | null {
|
||||
return this._workspaceIdentifier;
|
||||
}
|
||||
|
||||
load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
|
||||
this._workspaceIdentifier = workspaceIdentifier;
|
||||
return this.loadWorkspaceConfigurationContents(workspaceIdentifier)
|
||||
.then(contents => {
|
||||
this._workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(workspaceIdentifier.id);
|
||||
this._workspaceConfigurationModelParser.parse(contents);
|
||||
this.consolidate();
|
||||
});
|
||||
}
|
||||
|
||||
getConfigurationModel(): ConfigurationModel {
|
||||
return this._workspaceConfigurationModelParser.configurationModel;
|
||||
}
|
||||
|
||||
getFolders(): IStoredWorkspaceFolder[] {
|
||||
return this._workspaceConfigurationModelParser.folders;
|
||||
}
|
||||
|
||||
setFolders(folders: IStoredWorkspaceFolder[], jsonEditingService: JSONEditingService): Promise<void> {
|
||||
return jsonEditingService.write(this._workspaceConfigPath, { key: 'folders', value: folders }, true)
|
||||
.then(() => this.reload());
|
||||
}
|
||||
|
||||
getConfiguration(): ConfigurationModel {
|
||||
return this._cache;
|
||||
getWorkspaceSettings(): ConfigurationModel {
|
||||
return this._workspaceSettings;
|
||||
}
|
||||
|
||||
reprocessWorkspaceSettings(): ConfigurationModel {
|
||||
this._workspaceConfigurationModelParser.reprocessWorkspaceSettings();
|
||||
this.consolidate();
|
||||
return this.getConfiguration();
|
||||
}
|
||||
|
||||
private listenToWatcher() {
|
||||
this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire(), this, this._workspaceConfigurationWatcherDisposables);
|
||||
}
|
||||
|
||||
private stopListeningToWatcher() {
|
||||
this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables);
|
||||
return this.getWorkspaceSettings();
|
||||
}
|
||||
|
||||
private consolidate(): void {
|
||||
this._cache = this._workspaceConfigurationModelParser.settingsModel.merge(this._workspaceConfigurationModelParser.launchModel);
|
||||
this._workspaceSettings = this._workspaceConfigurationModelParser.settingsModel.merge(this._workspaceConfigurationModelParser.launchModel);
|
||||
}
|
||||
|
||||
private disposeConfigurationWatcher(): void {
|
||||
this.stopListeningToWatcher();
|
||||
if (this._workspaceConfigurationWatcher) {
|
||||
this._workspaceConfigurationWatcher.dispose();
|
||||
protected abstract loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise<string>;
|
||||
}
|
||||
|
||||
class NodeBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration {
|
||||
|
||||
protected loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise<string> {
|
||||
return pfs.readFile(workspaceIdentifier.configPath.fsPath)
|
||||
.then(contents => contents.toString(), e => {
|
||||
errors.onUnexpectedError(e);
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FileServiceBasedWorkspaceConfiguration extends AbstractWorkspaceConfiguration {
|
||||
|
||||
private workspaceConfig: URI | null = null;
|
||||
private readonly reloadConfigurationScheduler: RunOnceScheduler;
|
||||
|
||||
constructor(private fileService: IFileService, from?: AbstractWorkspaceConfiguration) {
|
||||
super(from);
|
||||
this.workspaceConfig = from ? from.workspaceIdentifier.configPath : null;
|
||||
this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e)));
|
||||
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50));
|
||||
}
|
||||
|
||||
protected loadWorkspaceConfigurationContents(workspaceIdentifier: IWorkspaceIdentifier): Promise<string> {
|
||||
this.workspaceConfig = workspaceIdentifier.configPath;
|
||||
return this.fileService.resolveContent(this.workspaceConfig)
|
||||
.then(content => content.value, e => {
|
||||
errors.onUnexpectedError(e);
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
private handleWorkspaceFileEvents(event: FileChangesEvent): void {
|
||||
if (this.workspaceConfig) {
|
||||
const events = event.changes;
|
||||
|
||||
let affectedByChanges = false;
|
||||
// Find changes that affect workspace file
|
||||
for (let i = 0, len = events.length; i < len && !affectedByChanges; i++) {
|
||||
affectedByChanges = resources.isEqual(this.workspaceConfig, events[i].resource);
|
||||
}
|
||||
|
||||
if (affectedByChanges) {
|
||||
this.reloadConfigurationScheduler.schedule();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CachedWorkspaceConfiguration extends Disposable implements IWorkspaceConfiguration {
|
||||
|
||||
private readonly _onDidChange: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
private cachedWorkspacePath: string;
|
||||
private cachedConfigurationPath: string;
|
||||
private _workspaceConfigurationModelParser: WorkspaceConfigurationModelParser;
|
||||
private _workspaceSettings: ConfigurationModel;
|
||||
|
||||
constructor(private environmentService: IEnvironmentService) {
|
||||
super();
|
||||
this._workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser('');
|
||||
this._workspaceSettings = new ConfigurationModel();
|
||||
}
|
||||
|
||||
load(workspaceIdentifier: IWorkspaceIdentifier): Promise<void> {
|
||||
this.createPaths(workspaceIdentifier);
|
||||
return pfs.readFile(this.cachedConfigurationPath)
|
||||
.then(contents => {
|
||||
this._workspaceConfigurationModelParser = new WorkspaceConfigurationModelParser(this.cachedConfigurationPath);
|
||||
this._workspaceConfigurationModelParser.parse(contents.toString());
|
||||
this._workspaceSettings = this._workspaceConfigurationModelParser.settingsModel.merge(this._workspaceConfigurationModelParser.launchModel);
|
||||
}, () => null);
|
||||
}
|
||||
|
||||
getConfigurationModel(): ConfigurationModel {
|
||||
return this._workspaceConfigurationModelParser.configurationModel;
|
||||
}
|
||||
|
||||
getFolders(): IStoredWorkspaceFolder[] {
|
||||
return this._workspaceConfigurationModelParser.folders;
|
||||
}
|
||||
|
||||
getWorkspaceSettings(): ConfigurationModel {
|
||||
return this._workspaceSettings;
|
||||
}
|
||||
|
||||
reprocessWorkspaceSettings(): ConfigurationModel {
|
||||
return this._workspaceSettings;
|
||||
}
|
||||
|
||||
async updateWorkspace(workspaceIdentifier: IWorkspaceIdentifier, configurationModel: ConfigurationModel): Promise<void> {
|
||||
try {
|
||||
this.createPaths(workspaceIdentifier);
|
||||
if (configurationModel.keys.length) {
|
||||
const exists = await pfs.exists(this.cachedWorkspacePath);
|
||||
if (!exists) {
|
||||
await pfs.mkdirp(this.cachedWorkspacePath);
|
||||
}
|
||||
const raw = JSON.stringify(configurationModel.toJSON());
|
||||
await pfs.writeFile(this.cachedConfigurationPath, raw);
|
||||
} else {
|
||||
pfs.rimraf(this.cachedWorkspacePath);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.onUnexpectedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposeConfigurationWatcher();
|
||||
super.dispose();
|
||||
private createPaths(workspaceIdentifier: IWorkspaceIdentifier) {
|
||||
this.cachedWorkspacePath = paths.join(this.environmentService.userDataPath, 'CachedConfigurations', 'workspaces', workspaceIdentifier.id);
|
||||
this.cachedConfigurationPath = paths.join(this.cachedWorkspacePath, 'workspace.json');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,12 +323,11 @@ function isFolderSettingsConfigurationFile(resource: URI): boolean {
|
||||
return resources.isEqual(URI.from({ scheme: resource.scheme, path: resources.basename(resource) }), URI.from({ scheme: resource.scheme, path: `${FOLDER_SETTINGS_NAME}.json` }));
|
||||
}
|
||||
|
||||
export interface IFolderConfiguration {
|
||||
export interface IFolderConfiguration extends IDisposable {
|
||||
readonly onDidChange: Event<void>;
|
||||
readonly loaded: boolean;
|
||||
loadConfiguration(): Promise<ConfigurationModel>;
|
||||
reprocess(): ConfigurationModel;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export abstract class AbstractFolderConfiguration extends Disposable implements IFolderConfiguration {
|
||||
@@ -225,13 +414,16 @@ export class NodeBasedFolderConfiguration extends AbstractFolderConfiguration {
|
||||
|
||||
protected loadFolderConfigurationContents(): Promise<{ resource: URI, value: string }[]> {
|
||||
return this.resolveStat(this.folderConfigurationPath).then(stat => {
|
||||
if (!stat.isDirectory) {
|
||||
if (!stat.isDirectory || !stat.children) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return this.resolveContents(stat.children.filter(stat => isFolderConfigurationFile(stat.resource))
|
||||
.map(stat => stat.resource));
|
||||
}, err => [] /* never fail this call */)
|
||||
.then(null, errors.onUnexpectedError);
|
||||
.then(undefined, e => {
|
||||
errors.onUnexpectedError(e);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
private resolveContents(resources: URI[]): Promise<{ resource: URI, value: string }[]> {
|
||||
@@ -265,7 +457,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
|
||||
|
||||
private reloadConfigurationScheduler: RunOnceScheduler;
|
||||
private readonly folderConfigurationPath: URI;
|
||||
private readonly loadConfigurationDelayer: Delayer<{ resource: URI, value: string }[]> = new Delayer<{ resource: URI, value: string }[]>(50);
|
||||
private readonly loadConfigurationDelayer = new Delayer<Array<{ resource: URI, value: string }>>(50);
|
||||
|
||||
constructor(folder: URI, private configFolderRelativePath: string, workbenchState: WorkbenchState, private fileService: IFileService, from?: AbstractFolderConfiguration) {
|
||||
super(folder, workbenchState, from);
|
||||
@@ -274,20 +466,25 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
|
||||
this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e)));
|
||||
}
|
||||
|
||||
protected loadFolderConfigurationContents(): Promise<{ resource: URI, value: string }[]> {
|
||||
protected loadFolderConfigurationContents(): Promise<Array<{ resource: URI, value: string }>> {
|
||||
return Promise.resolve(this.loadConfigurationDelayer.trigger(() => this.doLoadFolderConfigurationContents()));
|
||||
}
|
||||
|
||||
private doLoadFolderConfigurationContents(): Promise<{ resource: URI, value: string }[]> {
|
||||
private doLoadFolderConfigurationContents(): Promise<Array<{ resource: URI, value: string }>> {
|
||||
const workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: Promise<IContent> } = Object.create(null);
|
||||
const bulkContentFetchromise = Promise.resolve(this.fileService.resolveFile(this.folderConfigurationPath))
|
||||
.then(stat => {
|
||||
if (stat.isDirectory && stat.children) {
|
||||
stat.children
|
||||
.filter(child => isFolderConfigurationFile(child.resource))
|
||||
.forEach(child => workspaceFilePathToConfiguration[this.toFolderRelativePath(child.resource)] = Promise.resolve(this.fileService.resolveContent(child.resource)).then(null, errors.onUnexpectedError));
|
||||
.forEach(child => {
|
||||
const folderRelativePath = this.toFolderRelativePath(child.resource);
|
||||
if (folderRelativePath) {
|
||||
workspaceFilePathToConfiguration[folderRelativePath] = Promise.resolve(this.fileService.resolveContent(child.resource)).then(undefined, errors.onUnexpectedError);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).then(null, err => [] /* never fail this call */);
|
||||
}).then(undefined, err => [] /* never fail this call */);
|
||||
|
||||
return bulkContentFetchromise.then(() => Promise.all(collections.values(workspaceFilePathToConfiguration)));
|
||||
}
|
||||
@@ -298,7 +495,6 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
|
||||
|
||||
// Find changes that affect workspace configuration files
|
||||
for (let i = 0, len = events.length; i < len; i++) {
|
||||
|
||||
const resource = events[i].resource;
|
||||
const basename = resources.basename(resource);
|
||||
const isJson = paths.extname(basename) === '.json';
|
||||
@@ -316,6 +512,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
|
||||
// Handle case where ".vscode" got deleted
|
||||
if (isDeletedSettingsFolder) {
|
||||
affectedByChanges = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// only valid workspace config files
|
||||
@@ -323,12 +520,8 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (events[i].type) {
|
||||
case FileChangeType.DELETED:
|
||||
case FileChangeType.UPDATED:
|
||||
case FileChangeType.ADDED:
|
||||
affectedByChanges = true;
|
||||
}
|
||||
affectedByChanges = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (affectedByChanges) {
|
||||
@@ -336,7 +529,7 @@ export class FileServiceBasedFolderConfiguration extends AbstractFolderConfigura
|
||||
}
|
||||
}
|
||||
|
||||
private toFolderRelativePath(resource: URI): string {
|
||||
private toFolderRelativePath(resource: URI): string | null {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
if (paths.isEqualOrParent(resource.fsPath, this.folderConfigurationPath.fsPath, !isLinux /* ignorecase */)) {
|
||||
return paths.normalize(relative(this.folderConfigurationPath.fsPath, resource.fsPath));
|
||||
@@ -366,7 +559,7 @@ export class CachedFolderConfiguration extends Disposable implements IFolderConf
|
||||
configFolderRelativePath: string,
|
||||
environmentService: IEnvironmentService) {
|
||||
super();
|
||||
this.cachedFolderPath = paths.join(environmentService.appSettingsHome, createHash('md5').update(paths.join(folder.path, configFolderRelativePath)).digest('hex'));
|
||||
this.cachedFolderPath = paths.join(environmentService.userDataPath, 'CachedConfigurations', 'folders', createHash('md5').update(paths.join(folder.path, configFolderRelativePath)).digest('hex'));
|
||||
this.cachedConfigurationPath = paths.join(this.cachedFolderPath, 'configuration.json');
|
||||
this.configurationModel = new ConfigurationModel();
|
||||
}
|
||||
@@ -387,7 +580,7 @@ export class CachedFolderConfiguration extends Disposable implements IFolderConf
|
||||
if (created) {
|
||||
return configurationModel.keys.length ? pfs.writeFile(this.cachedConfigurationPath, raw) : pfs.rimraf(this.cachedFolderPath);
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -401,7 +594,7 @@ export class CachedFolderConfiguration extends Disposable implements IFolderConf
|
||||
|
||||
private createCachedFolder(): Promise<boolean> {
|
||||
return Promise.resolve(pfs.exists(this.cachedFolderPath))
|
||||
.then(null, () => false)
|
||||
.then(undefined, () => false)
|
||||
.then(exists => exists ? exists : pfs.mkdirp(this.cachedFolderPath).then(() => true, () => false));
|
||||
}
|
||||
}
|
||||
@@ -492,6 +685,6 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
|
||||
return this.folderConfiguration.loadConfiguration()
|
||||
.then(configurationModel => this.cachedFolderConfiguration.updateConfiguration(configurationModel));
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,15 +122,15 @@ export class ConfigurationEditingService {
|
||||
private queue: Queue<void>;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IEnvironmentService private environmentService: IEnvironmentService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@ITextModelService private textModelResolverService: ITextModelService,
|
||||
@ITextFileService private textFileService: ITextFileService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IPreferencesService private preferencesService: IPreferencesService,
|
||||
@IEditorService private editorService: IEditorService
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@ITextModelService private readonly textModelResolverService: ITextModelService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IPreferencesService private readonly preferencesService: IPreferencesService,
|
||||
@IEditorService private readonly editorService: IEditorService
|
||||
) {
|
||||
this.queue = new Queue<void>();
|
||||
}
|
||||
@@ -264,7 +264,7 @@ export class ConfigurationEditingService {
|
||||
this.editorService.openEditor({ resource });
|
||||
}
|
||||
|
||||
private wrapError<T = never>(code: ConfigurationEditingErrorCode, target: ConfigurationTarget, operation: IConfigurationEditOperation): Promise<T> {
|
||||
private reject<T = never>(code: ConfigurationEditingErrorCode, target: ConfigurationTarget, operation: IConfigurationEditOperation): Promise<T> {
|
||||
const message = this.toErrorMessage(code, target, operation);
|
||||
|
||||
return Promise.reject(new ConfigurationEditingError(message, code));
|
||||
@@ -377,45 +377,45 @@ export class ConfigurationEditingService {
|
||||
if (!operation.workspaceStandAloneConfigurationKey) {
|
||||
const validKeys = this.configurationService.keys().default;
|
||||
if (validKeys.indexOf(operation.key) < 0 && !OVERRIDE_PROPERTY_PATTERN.test(operation.key)) {
|
||||
return this.wrapError(ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY, target, operation);
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY, target, operation);
|
||||
}
|
||||
}
|
||||
|
||||
if (operation.workspaceStandAloneConfigurationKey) {
|
||||
// Global tasks and launches are not supported
|
||||
if (target === ConfigurationTarget.USER) {
|
||||
return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, target, operation);
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, target, operation);
|
||||
}
|
||||
|
||||
// Workspace tasks are not supported
|
||||
if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && operation.target === ConfigurationTarget.WORKSPACE) {
|
||||
return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET, target, operation);
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_TARGET, target, operation);
|
||||
}
|
||||
}
|
||||
|
||||
// Target cannot be workspace or folder if no workspace opened
|
||||
if ((target === ConfigurationTarget.WORKSPACE || target === ConfigurationTarget.WORKSPACE_FOLDER) && this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
return this.wrapError(ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED, target, operation);
|
||||
return this.reject(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);
|
||||
return this.reject(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);
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation);
|
||||
}
|
||||
|
||||
if (!operation.workspaceStandAloneConfigurationKey) {
|
||||
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
|
||||
if (configurationProperties[operation.key].scope !== ConfigurationScope.RESOURCE) {
|
||||
return this.wrapError(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, target, operation);
|
||||
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, target, operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -425,12 +425,12 @@ export class ConfigurationEditingService {
|
||||
const model = reference.object.textEditorModel;
|
||||
|
||||
if (this.hasParseErrors(model, operation)) {
|
||||
return this.wrapError<typeof reference>(ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION, target, operation);
|
||||
return this.reject<typeof reference>(ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION, target, operation);
|
||||
}
|
||||
|
||||
// Target cannot be dirty if not writing into buffer
|
||||
if (checkDirty && this.textFileService.isDirty(operation.resource)) {
|
||||
return this.wrapError<typeof reference>(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, target, operation);
|
||||
return this.reject<typeof reference>(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, target, operation);
|
||||
}
|
||||
return reference;
|
||||
});
|
||||
@@ -441,8 +441,7 @@ export class ConfigurationEditingService {
|
||||
// Check for standalone workspace configurations
|
||||
if (config.key) {
|
||||
const standaloneConfigurationKeys = Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS);
|
||||
for (let i = 0; i < standaloneConfigurationKeys.length; i++) {
|
||||
const key = standaloneConfigurationKeys[i];
|
||||
for (const key of standaloneConfigurationKeys) {
|
||||
const resource = this.getConfigurationFileResource(target, WORKSPACE_STANDALONE_CONFIGURATIONS[key], overrides.resource);
|
||||
|
||||
// Check for prefix
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { dirname } from 'path';
|
||||
import * as assert from 'vs/base/common/assert';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
@@ -36,7 +35,7 @@ import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/works
|
||||
import { UserConfiguration } from 'vs/platform/configuration/node/configuration';
|
||||
import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema';
|
||||
import { localize } from 'vs/nls';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { isEqual, dirname } from 'vs/base/common/resources';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
|
||||
export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService {
|
||||
@@ -44,6 +43,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
public _serviceBrand: any;
|
||||
|
||||
private workspace: Workspace;
|
||||
private resolvePromise: Promise<void>;
|
||||
private resolveCallback: () => void;
|
||||
private _configuration: Configuration;
|
||||
private defaultConfiguration: DefaultConfigurationModel;
|
||||
private userConfiguration: UserConfiguration;
|
||||
@@ -52,7 +53,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
|
||||
private workspaceEditingQueue: Queue<void>;
|
||||
|
||||
protected readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>({ leakWarningThreshold: 500 }));
|
||||
protected readonly _onDidChangeConfiguration: Emitter<IConfigurationChangeEvent> = this._register(new Emitter<IConfigurationChangeEvent>());
|
||||
public readonly onDidChangeConfiguration: Event<IConfigurationChangeEvent> = this._onDidChangeConfiguration.event;
|
||||
|
||||
protected readonly _onDidChangeWorkspaceFolders: Emitter<IWorkspaceFoldersChangeEvent> = this._register(new Emitter<IWorkspaceFoldersChangeEvent>());
|
||||
@@ -71,20 +72,25 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
constructor(private environmentService: IEnvironmentService, private workspaceSettingsRootFolder: string = FOLDER_CONFIG_FOLDER_NAME) {
|
||||
super();
|
||||
|
||||
this.resolvePromise = new Promise(c => this.resolveCallback = c);
|
||||
this.defaultConfiguration = new DefaultConfigurationModel();
|
||||
this.userConfiguration = this._register(new UserConfiguration(environmentService.appSettingsPath));
|
||||
this.workspaceConfiguration = this._register(new WorkspaceConfiguration());
|
||||
this.workspaceConfiguration = this._register(new WorkspaceConfiguration(environmentService));
|
||||
this._register(this.userConfiguration.onDidChangeConfiguration(userConfiguration => this.onUserConfigurationChanged(userConfiguration)));
|
||||
this._register(this.workspaceConfiguration.onDidUpdateConfiguration(() => this.onWorkspaceConfigurationChanged()));
|
||||
|
||||
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidSchemaChange(e => this.registerConfigurationSchemas()));
|
||||
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidRegisterConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties)));
|
||||
this._register(Registry.as<IConfigurationRegistry>(Extensions.Configuration).onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties)));
|
||||
|
||||
this.workspaceEditingQueue = new Queue<void>();
|
||||
}
|
||||
|
||||
// Workspace Context Service Impl
|
||||
|
||||
public getCompleteWorkspace(): Promise<Workspace> {
|
||||
return this.resolvePromise.then(() => this.getWorkspace());
|
||||
}
|
||||
|
||||
public getWorkspace(): Workspace {
|
||||
return this.workspace;
|
||||
}
|
||||
@@ -137,11 +143,11 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
|
||||
private doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise<void> {
|
||||
if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
|
||||
return Promise.resolve(void 0); // we need a workspace to begin with
|
||||
return Promise.resolve(undefined); // we need a workspace to begin with
|
||||
}
|
||||
|
||||
if (foldersToAdd.length + foldersToRemove.length === 0) {
|
||||
return Promise.resolve(void 0); // nothing to do
|
||||
return Promise.resolve(undefined); // nothing to do
|
||||
}
|
||||
|
||||
let foldersHaveChanged = false;
|
||||
@@ -162,8 +168,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
if (foldersToAdd.length) {
|
||||
|
||||
// Recompute current workspace folders if we have folders to add
|
||||
const workspaceConfigFolder = dirname(this.getWorkspace().configuration.fsPath);
|
||||
currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, URI.file(workspaceConfigFolder));
|
||||
const workspaceConfigFolder = dirname(this.getWorkspace().configuration);
|
||||
currentWorkspaceFolders = toWorkspaceFolders(newStoredFolders, workspaceConfigFolder);
|
||||
const currentWorkspaceFolderUris = currentWorkspaceFolders.map(folder => folder.uri);
|
||||
|
||||
const storedFoldersToAdd: IStoredWorkspaceFolder[] = [];
|
||||
@@ -214,7 +220,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
return this.setFolders(newStoredFolders);
|
||||
}
|
||||
|
||||
return Promise.resolve(void 0);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private setFolders(folders: IStoredWorkspaceFolder[]): Promise<void> {
|
||||
@@ -245,8 +251,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
getValue<T>(overrides: IConfigurationOverrides): T;
|
||||
getValue<T>(section: string, overrides: IConfigurationOverrides): T;
|
||||
getValue(arg1?: any, arg2?: any): any {
|
||||
const section = typeof arg1 === 'string' ? arg1 : void 0;
|
||||
const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : void 0;
|
||||
const section = typeof arg1 === 'string' ? arg1 : undefined;
|
||||
const overrides = isConfigurationOverrides(arg1) ? arg1 : isConfigurationOverrides(arg2) ? arg2 : undefined;
|
||||
return this._configuration.getValue(section, overrides);
|
||||
}
|
||||
|
||||
@@ -257,7 +263,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
updateValue(key: string, value: any, overrides: IConfigurationOverrides, target: ConfigurationTarget, donotNotifyError: boolean): Promise<void>;
|
||||
updateValue(key: string, value: any, arg3?: any, arg4?: any, donotNotifyError?: any): Promise<void> {
|
||||
assert.ok(this.configurationEditingService, 'Workbench is not initialized yet');
|
||||
const overrides = isConfigurationOverrides(arg3) ? arg3 : void 0;
|
||||
const overrides = isConfigurationOverrides(arg3) ? arg3 : undefined;
|
||||
const target = this.deriveConfigurationTarget(key, value, overrides, overrides ? arg4 : arg3);
|
||||
return target ? this.writeConfigurationValue(key, value, target, overrides, donotNotifyError)
|
||||
: Promise.resolve(null);
|
||||
@@ -303,17 +309,22 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
acquireFileService(fileService: IFileService): void {
|
||||
this.fileService = fileService;
|
||||
const changedWorkspaceFolders: IWorkspaceFolder[] = [];
|
||||
Promise.all(this.cachedFolderConfigs.values()
|
||||
Promise.all([this.workspaceConfiguration.adopt(fileService), ...this.cachedFolderConfigs.values()
|
||||
.map(folderConfiguration => folderConfiguration.adopt(fileService)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
changedWorkspaceFolders.push(folderConfiguration.workspaceFolder);
|
||||
}
|
||||
})))
|
||||
.then(() => {
|
||||
return result;
|
||||
}))])
|
||||
.then(([workspaceChanged]) => {
|
||||
if (workspaceChanged) {
|
||||
this.onWorkspaceConfigurationChanged();
|
||||
}
|
||||
for (const workspaceFolder of changedWorkspaceFolders) {
|
||||
this.onWorkspaceFolderConfigurationChanged(workspaceFolder);
|
||||
}
|
||||
this.resolveCallback();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -335,10 +346,10 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
}
|
||||
|
||||
private createMultiFolderWorkspace(workspaceIdentifier: IWorkspaceIdentifier): Promise<Workspace> {
|
||||
const workspaceConfigPath = URI.file(workspaceIdentifier.configPath);
|
||||
return this.workspaceConfiguration.load(workspaceConfigPath)
|
||||
return this.workspaceConfiguration.load({ id: workspaceIdentifier.id, configPath: URI.file(workspaceIdentifier.configPath) })
|
||||
.then(() => {
|
||||
const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(dirname(workspaceConfigPath.fsPath)));
|
||||
const workspaceConfigPath = URI.file(workspaceIdentifier.configPath);
|
||||
const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), dirname(workspaceConfigPath));
|
||||
const workspaceId = workspaceIdentifier.id;
|
||||
return new Workspace(workspaceId, workspaceFolders, workspaceConfigPath);
|
||||
});
|
||||
@@ -369,7 +380,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
|
||||
if (hasWorkspaceBefore) {
|
||||
previousState = this.getWorkbenchState();
|
||||
previousWorkspacePath = this.workspace.configuration ? this.workspace.configuration.fsPath : void 0;
|
||||
previousWorkspacePath = this.workspace.configuration ? this.workspace.configuration.fsPath : undefined;
|
||||
previousFolders = this.workspace.folders;
|
||||
this.workspace.update(workspace);
|
||||
} else {
|
||||
@@ -387,7 +398,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
this._onDidChangeWorkbenchState.fire(newState);
|
||||
}
|
||||
|
||||
const newWorkspacePath = this.workspace.configuration ? this.workspace.configuration.fsPath : void 0;
|
||||
const newWorkspacePath = this.workspace.configuration ? this.workspace.configuration.fsPath : undefined;
|
||||
if (previousWorkspacePath && newWorkspacePath !== previousWorkspacePath || newState !== previousState) {
|
||||
this._onDidChangeWorkspaceName.fire();
|
||||
}
|
||||
@@ -436,7 +447,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
if (workbenchState === WorkbenchState.WORKSPACE) {
|
||||
return this.workspaceConfiguration.reload().then(() => this.onWorkspaceConfigurationChanged());
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private reloadWorkspaceFolderConfiguration(folder: IWorkspaceFolder, key?: string): Promise<void> {
|
||||
@@ -531,7 +542,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
private onWorkspaceConfigurationChanged(): Promise<void> {
|
||||
if (this.workspace && this.workspace.configuration && this._configuration) {
|
||||
const workspaceConfigurationChangeEvent = this._configuration.compareAndUpdateWorkspaceConfiguration(this.workspaceConfiguration.getConfiguration());
|
||||
let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(dirname(this.workspace.configuration.fsPath)));
|
||||
let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), dirname(this.workspace.configuration));
|
||||
const changes = this.compareFolders(this.workspace.folders, configuredFolders);
|
||||
if (changes.added.length || changes.removed.length || changes.changed.length) {
|
||||
this.workspace.folders = configuredFolders;
|
||||
@@ -544,7 +555,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
this.triggerConfigurationChange(workspaceConfigurationChangeEvent, ConfigurationTarget.WORKSPACE);
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
private onWorkspaceFolderConfigurationChanged(folder: IWorkspaceFolder, key?: string): Promise<void> {
|
||||
@@ -606,7 +617,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
if (target === ConfigurationTarget.MEMORY) {
|
||||
this._configuration.updateValue(key, value, overrides);
|
||||
this.triggerConfigurationChange(new ConfigurationChangeEvent().change(overrides.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier)] : [key], overrides.resource), target);
|
||||
return Promise.resolve(null);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return this.configurationEditingService.writeConfiguration(target, { key, value }, { scopes: overrides, donotNotifyError })
|
||||
@@ -631,22 +642,22 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat
|
||||
return target;
|
||||
}
|
||||
|
||||
if (value === void 0) {
|
||||
if (value === undefined) {
|
||||
// Ignore. But expected is to remove the value from all targets
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const inspect = this.inspect(key, overrides);
|
||||
if (equals(value, inspect.value)) {
|
||||
// No change. So ignore.
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (inspect.workspaceFolder !== void 0) {
|
||||
if (inspect.workspaceFolder !== undefined) {
|
||||
return ConfigurationTarget.WORKSPACE_FOLDER;
|
||||
}
|
||||
|
||||
if (inspect.workspace !== void 0) {
|
||||
if (inspect.workspace !== undefined) {
|
||||
return ConfigurationTarget.WORKSPACE;
|
||||
}
|
||||
|
||||
@@ -693,8 +704,8 @@ export class DefaultConfigurationExportHelper {
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IExtensionService private extensionService: IExtensionService,
|
||||
@ICommandService private commandService: ICommandService) {
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@ICommandService private readonly commandService: ICommandService) {
|
||||
if (environmentService.args['export-default-configuration']) {
|
||||
this.writeConfigModelAndQuit(environmentService.args['export-default-configuration']);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ export class JSONEditingService implements IJSONEditingService {
|
||||
private queue: Queue<void>;
|
||||
|
||||
constructor(
|
||||
@IFileService private fileService: IFileService,
|
||||
@ITextModelService private textModelResolverService: ITextModelService,
|
||||
@ITextFileService private textFileService: ITextFileService
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@ITextModelService private readonly textModelResolverService: ITextModelService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService
|
||||
) {
|
||||
this.queue = new Queue<void>();
|
||||
}
|
||||
@@ -103,18 +103,18 @@ export class JSONEditingService implements IJSONEditingService {
|
||||
const model = reference.object.textEditorModel;
|
||||
|
||||
if (this.hasParseErrors(model)) {
|
||||
return this.wrapError<IReference<ITextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);
|
||||
return this.reject<IReference<ITextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);
|
||||
}
|
||||
|
||||
// Target cannot be dirty if not writing into buffer
|
||||
if (checkDirty && this.textFileService.isDirty(resource)) {
|
||||
return this.wrapError<IReference<ITextEditorModel>>(JSONEditingErrorCode.ERROR_FILE_DIRTY);
|
||||
return this.reject<IReference<ITextEditorModel>>(JSONEditingErrorCode.ERROR_FILE_DIRTY);
|
||||
}
|
||||
return reference;
|
||||
});
|
||||
}
|
||||
|
||||
private wrapError<T>(code: JSONEditingErrorCode): Promise<T> {
|
||||
private reject<T>(code: JSONEditingErrorCode): Promise<T> {
|
||||
const message = this.toErrorMessage(code);
|
||||
return Promise.reject(new JSONEditingError(message, code));
|
||||
}
|
||||
|
||||
@@ -201,7 +201,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 ResourceMap(), new ConfigurationModel(), new ResourceMap(), 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']);
|
||||
|
||||
@@ -123,18 +123,18 @@ suite('ConfigurationEditingService', () => {
|
||||
if (configuraitonService) {
|
||||
configuraitonService.dispose();
|
||||
}
|
||||
instantiationService = null;
|
||||
instantiationService = null!;
|
||||
}
|
||||
}
|
||||
|
||||
function clearWorkspace(): Promise<void> {
|
||||
return new Promise<void>((c, e) => {
|
||||
if (parentDir) {
|
||||
extfs.del(parentDir, os.tmpdir(), () => c(null), () => c(null));
|
||||
extfs.del(parentDir, os.tmpdir(), () => c(undefined), () => c(undefined));
|
||||
} else {
|
||||
c(null);
|
||||
c(undefined);
|
||||
}
|
||||
}).then(() => parentDir = null);
|
||||
}).then(() => parentDir = null!);
|
||||
}
|
||||
|
||||
test('errors cases - invalid key', () => {
|
||||
@@ -179,7 +179,7 @@ suite('ConfigurationEditingService', () => {
|
||||
test('do not notify error', () => {
|
||||
instantiationService.stub(ITextFileService, 'isDirty', true);
|
||||
const target = sinon.stub();
|
||||
instantiationService.stub(INotificationService, <INotificationService>{ prompt: target, _serviceBrand: null, notify: null, error: null, info: null, warn: null });
|
||||
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) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur
|
||||
import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService';
|
||||
import { ISingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/node/configurationEditingService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IFileService, FileChangesEvent, FileChangeType } 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, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
|
||||
@@ -34,6 +34,7 @@ import { JSONEditingService } from 'vs/workbench/services/configuration/node/jso
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { Uri } from 'vscode';
|
||||
import { createHash } from 'crypto';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
|
||||
class SettingsTestEnvironmentService extends EnvironmentService {
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
@@ -26,29 +25,39 @@ export interface IConfigurationResolverService {
|
||||
/**
|
||||
* Recursively resolves all variables (including commands and user input) in the given config and returns a copy of it with substituted values.
|
||||
* If a "variables" dictionary (with names -> command ids) is given, command variables are first mapped through it before being resolved.
|
||||
* @param folder
|
||||
* @param config
|
||||
*
|
||||
* @param section For example, 'tasks' or 'debug'. Used for resolving inputs.
|
||||
* @param variables Aliases for commands.
|
||||
*/
|
||||
resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): TPromise<any>;
|
||||
resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): Promise<any>;
|
||||
|
||||
/**
|
||||
* Similar to resolveWithInteractionReplace, except without the replace. Returns a map of variables and their resolution.
|
||||
* Keys in the map will be of the format input:variableName or command:variableName.
|
||||
*/
|
||||
resolveWithInteraction(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): TPromise<Map<string, string>>;
|
||||
resolveWithInteraction(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): Promise<Map<string, string>>;
|
||||
}
|
||||
|
||||
export const enum ConfiguredInputType {
|
||||
PromptString,
|
||||
PickString
|
||||
}
|
||||
|
||||
export interface ConfiguredInput {
|
||||
export interface PromptStringInputInfo {
|
||||
id: string;
|
||||
type: 'promptString';
|
||||
description: string;
|
||||
default?: string;
|
||||
type: ConfiguredInputType;
|
||||
options?: string[];
|
||||
}
|
||||
}
|
||||
|
||||
export interface PickStringInputInfo {
|
||||
id: string;
|
||||
type: 'pickString';
|
||||
description: string;
|
||||
options: string[];
|
||||
default?: string;
|
||||
}
|
||||
|
||||
export interface CommandInputInfo {
|
||||
id: string;
|
||||
type: 'command';
|
||||
command: string;
|
||||
args?: any;
|
||||
}
|
||||
|
||||
export type ConfiguredInput = PromptStringInputInfo | PickStringInputInfo | CommandInputInfo;
|
||||
@@ -5,41 +5,109 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
const idDescription = nls.localize('JsonSchema.input.id', "The input's id is used to associate an input with a variable of the form ${input:id}.");
|
||||
const typeDescription = nls.localize('JsonSchema.input.type', "The type of user input prompt to use.");
|
||||
const descriptionDescription = nls.localize('JsonSchema.input.description', "The description is shown when the user is prompted for input.");
|
||||
const defaultDescription = nls.localize('JsonSchema.input.default', "The default value for the input.");
|
||||
|
||||
|
||||
export const inputsSchema: IJSONSchema = {
|
||||
definitions: {
|
||||
inputs: {
|
||||
type: 'array',
|
||||
description: nls.localize('JsonSchema.inputs', 'User inputs. Used for defining user input prompts, such as free string input or a choice from several options.'),
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['id', 'type', 'description'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: nls.localize('JsonSchema.input.id', "The input\'s id is used to specify inputs as ${input:id}.")
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
required: ['id', 'type', 'description'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: idDescription
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: typeDescription,
|
||||
enum: ['promptString'],
|
||||
enumDescriptions: [
|
||||
nls.localize('JsonSchema.input.type.promptString', "The 'promptString' type opens an input box to ask the user for input."),
|
||||
]
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: descriptionDescription
|
||||
},
|
||||
default: {
|
||||
type: 'string',
|
||||
description: defaultDescription
|
||||
},
|
||||
}
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: nls.localize('JsonSchema.input.type', 'The promptString type opens an input box to ask the user for input. The pickString type shows a selection list.'),
|
||||
enum: ['promptString', 'pickString']
|
||||
{
|
||||
type: 'object',
|
||||
required: ['id', 'type', 'description', 'options'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: idDescription
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: typeDescription,
|
||||
enum: ['pickString'],
|
||||
enumDescriptions: [
|
||||
nls.localize('JsonSchema.input.type.pickString', "The 'pickString' type shows a selection list."),
|
||||
]
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: descriptionDescription
|
||||
},
|
||||
default: {
|
||||
type: 'string',
|
||||
description: defaultDescription
|
||||
},
|
||||
options: {
|
||||
type: 'array',
|
||||
description: nls.localize('JsonSchema.input.options', 'An array of strings that defines the options for a quick pick.'),
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: nls.localize('JsonSchema.input.description', 'The description is shown when the user is prompted for input.'),
|
||||
},
|
||||
default: {
|
||||
type: 'string',
|
||||
description: nls.localize('JsonSchema.input.default', 'The default value for the input.'),
|
||||
},
|
||||
options: {
|
||||
type: 'array',
|
||||
description: nls.localize('JsonSchema.input.options', 'An array of strings that defines the options for a quick pick.'),
|
||||
items: {
|
||||
type: 'string'
|
||||
{
|
||||
type: 'object',
|
||||
required: ['id', 'type', 'command'],
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
description: idDescription
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
description: typeDescription,
|
||||
enum: ['command'],
|
||||
enumDescriptions: [
|
||||
nls.localize('JsonSchema.input.type.command', "The 'command' type executes a command."),
|
||||
]
|
||||
},
|
||||
command: {
|
||||
type: 'string',
|
||||
description: nls.localize('JsonSchema.input.command.command', "The command to execute for this input variable.")
|
||||
},
|
||||
args: {
|
||||
type: 'object',
|
||||
description: nls.localize('JsonSchema.input.command.args', "Optional arguments passed to the command.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,8 @@ import { URI as uri } from 'vs/base/common/uri';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as paths from 'vs/base/common/paths';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import * as Objects from 'vs/base/common/objects';
|
||||
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, forEach, fromMap } from 'vs/base/common/collections';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -23,18 +20,20 @@ import { AbstractVariableResolverService } from 'vs/workbench/services/configura
|
||||
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
|
||||
import { IQuickInputService, IInputOptions, IQuickPickItem, IPickOptions } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { ConfiguredInput, ConfiguredInputType } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
import { ConfiguredInput } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
|
||||
export class ConfigurationResolverService extends AbstractVariableResolverService {
|
||||
|
||||
static INPUT_OR_COMMAND_VARIABLES_PATTERN = /\${((input|command):(.*?))}/g;
|
||||
|
||||
constructor(
|
||||
envVariables: platform.IProcessEnvironment,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@ICommandService private commandService: ICommandService,
|
||||
@IWorkspaceContextService private workspaceContextService: IWorkspaceContextService,
|
||||
@IQuickInputService private quickInputService: IQuickInputService
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IQuickInputService private readonly quickInputService: IQuickInputService
|
||||
) {
|
||||
super({
|
||||
getFolderUri: (folderName: string): uri => {
|
||||
@@ -83,7 +82,7 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic
|
||||
}, envVariables);
|
||||
}
|
||||
|
||||
public resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): TPromise<any> {
|
||||
public resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): Promise<any> {
|
||||
// resolve any non-interactive variables
|
||||
config = this.resolveAny(folder, config);
|
||||
|
||||
@@ -100,26 +99,18 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic
|
||||
});
|
||||
}
|
||||
|
||||
public resolveWithInteraction(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): TPromise<Map<string, string>> {
|
||||
public resolveWithInteraction(folder: IWorkspaceFolder, config: any, section?: string, variables?: IStringDictionary<string>): Promise<Map<string, string>> {
|
||||
// resolve any non-interactive variables
|
||||
const resolved = this.resolveAnyMap(folder, config);
|
||||
config = resolved.newConfig;
|
||||
const allVariableMapping: Map<string, string> = resolved.resolvedVariables;
|
||||
|
||||
// resolve input variables in the order in which they are encountered
|
||||
return this.resolveWithInputs(folder, config, section).then(inputMapping => {
|
||||
if (!this.updateMapping(inputMapping, allVariableMapping)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// resolve commands in the order in which they are encountered
|
||||
return this.resolveWithCommands(config, variables).then(commandMapping => {
|
||||
if (!this.updateMapping(commandMapping, allVariableMapping)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// resolve input and command variables in the order in which they are encountered
|
||||
return this.resolveWithInputAndCommands(folder, config, variables, section).then(inputOrCommandMapping => {
|
||||
if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) {
|
||||
return allVariableMapping;
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,199 +128,163 @@ export class ConfigurationResolverService extends AbstractVariableResolverServic
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and executes all command variables in the given configuration and returns their values as a dictionary.
|
||||
* Please note: this method does not substitute the command variables (so the configuration is not modified).
|
||||
* The returned dictionary can be passed to "resolvePlatform" for the substitution.
|
||||
* Finds and executes all input and command variables in the given configuration and returns their values as a dictionary.
|
||||
* Please note: this method does not substitute the input or command variables (so the configuration is not modified).
|
||||
* The returned dictionary can be passed to "resolvePlatform" for the actual substitution.
|
||||
* See #6569.
|
||||
* @param configuration
|
||||
*
|
||||
* @param variableToCommandMap Aliases for commands
|
||||
*/
|
||||
private resolveWithCommands(configuration: any, variableToCommandMap: IStringDictionary<string>): TPromise<IStringDictionary<string>> {
|
||||
private async resolveWithInputAndCommands(folder: IWorkspaceFolder, configuration: any, variableToCommandMap: IStringDictionary<string>, section: string): Promise<IStringDictionary<string>> {
|
||||
|
||||
if (!configuration) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
// use an array to preserve order of first appearance
|
||||
const cmd_var = /\${command:(.*?)}/g;
|
||||
const commands: string[] = [];
|
||||
this.findVariables(cmd_var, configuration, commands);
|
||||
let cancelled = false;
|
||||
const commandValueMapping: IStringDictionary<string> = Object.create(null);
|
||||
|
||||
const factory: { (): TPromise<any> }[] = commands.map(commandVariable => {
|
||||
return () => {
|
||||
let commandId = variableToCommandMap ? variableToCommandMap[commandVariable] : undefined;
|
||||
if (!commandId) {
|
||||
// Just launch any command if the interactive variable is not contributed by the adapter #12735
|
||||
commandId = commandVariable;
|
||||
}
|
||||
|
||||
return this.commandService.executeCommand<string>(commandId, configuration).then(result => {
|
||||
if (typeof result === 'string') {
|
||||
commandValueMapping['command:' + commandVariable] = result;
|
||||
} else if (Types.isUndefinedOrNull(result)) {
|
||||
cancelled = true;
|
||||
} else {
|
||||
throw new Error(nls.localize('stringsOnlySupported', "Command '{0}' did not return a string result. Only strings are supported as results for commands used for variable substitution.", commandVariable));
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return sequence(factory).then(() => cancelled ? undefined : commandValueMapping);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all inputs in a configuration and returns a map that maps the unresolved input to the resolved input.
|
||||
* Does not do replacement of inputs.
|
||||
* @param folder
|
||||
* @param config
|
||||
* @param section
|
||||
*/
|
||||
public resolveWithInputs(folder: IWorkspaceFolder, config: any, section: string): Promise<IStringDictionary<string>> {
|
||||
if (!config) {
|
||||
return Promise.resolve(undefined);
|
||||
} else if (folder && section) {
|
||||
// Get all the possible inputs
|
||||
let result = this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY
|
||||
? Objects.deepClone(this.configurationService.getValue<any>(section, { resource: folder.uri }))
|
||||
: undefined;
|
||||
let inputsArray = result ? this.parseConfigurationInputs(result.inputs) : undefined;
|
||||
const inputs = new Map<string, ConfiguredInput>();
|
||||
if (inputsArray) {
|
||||
inputsArray.forEach(input => {
|
||||
inputs.set(input.id, input);
|
||||
});
|
||||
}
|
||||
|
||||
// use an array to preserve order of first appearance
|
||||
const input_var = /\${input:(.*?)}/g;
|
||||
const commands: string[] = [];
|
||||
this.findVariables(input_var, config, commands);
|
||||
let cancelled = false;
|
||||
const commandValueMapping: IStringDictionary<string> = Object.create(null);
|
||||
|
||||
const factory: { (): Promise<any> }[] = commands.map(commandVariable => {
|
||||
return () => {
|
||||
return this.showUserInput(commandVariable, inputs).then(resolvedValue => {
|
||||
if (resolvedValue) {
|
||||
commandValueMapping['input:' + commandVariable] = resolvedValue;
|
||||
} else {
|
||||
cancelled = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
}, reason => {
|
||||
return Promise.reject(reason);
|
||||
});
|
||||
|
||||
return sequence(factory).then(() => cancelled ? undefined : commandValueMapping);
|
||||
// get all "inputs"
|
||||
let inputs: ConfiguredInput[] = undefined;
|
||||
if (folder && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY) {
|
||||
let result = this.configurationService.getValue<any>(section, { resource: folder.uri });
|
||||
if (result) {
|
||||
inputs = result.inputs;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(Object.create(null));
|
||||
}
|
||||
// extract and dedupe all "input" and "command" variables and preserve their order in an array
|
||||
const variables: string[] = [];
|
||||
this.findVariables(configuration, variables);
|
||||
|
||||
/**
|
||||
* Takes the provided input info and shows the quick pick so the user can provide the value for the input
|
||||
* @param commandVariable Name of the input.
|
||||
* @param inputs Information about each possible input.
|
||||
* @param commandValueMapping
|
||||
*/
|
||||
private showUserInput(commandVariable: string, inputs: Map<string, ConfiguredInput>): Promise<string> {
|
||||
if (inputs && inputs.has(commandVariable)) {
|
||||
const input = inputs.get(commandVariable);
|
||||
if (input.type === ConfiguredInputType.PromptString) {
|
||||
let inputOptions: IInputOptions = { prompt: input.description };
|
||||
if (input.default) {
|
||||
inputOptions.value = input.default;
|
||||
}
|
||||
const variableValues: IStringDictionary<string> = Object.create(null);
|
||||
|
||||
return this.quickInputService.input(inputOptions).then(resolvedInput => {
|
||||
return resolvedInput ? resolvedInput : undefined;
|
||||
});
|
||||
} else { // input.type === ConfiguredInputType.pick
|
||||
let picks = new Array<IQuickPickItem>();
|
||||
if (input.options) {
|
||||
input.options.forEach(pickOption => {
|
||||
let item: IQuickPickItem = { label: pickOption };
|
||||
if (input.default && (pickOption === input.default)) {
|
||||
item.description = nls.localize('defaultInputValue', "Default");
|
||||
picks.unshift(item);
|
||||
} else {
|
||||
picks.push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
let pickOptions: IPickOptions<IQuickPickItem> = { placeHolder: input.description };
|
||||
return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => {
|
||||
return resolvedInput ? resolvedInput.label : undefined;
|
||||
});
|
||||
for (const variable of variables) {
|
||||
|
||||
const [type, name] = variable.split(':', 2);
|
||||
|
||||
let result: string | undefined | null;
|
||||
|
||||
switch (type) {
|
||||
|
||||
case 'input':
|
||||
result = await this.showUserInput(name, inputs);
|
||||
break;
|
||||
|
||||
case 'command':
|
||||
// use the name as a command ID #12735
|
||||
const commandId = (variableToCommandMap ? variableToCommandMap[name] : undefined) || name;
|
||||
result = await this.commandService.executeCommand(commandId, configuration);
|
||||
if (typeof result !== 'string' && !Types.isUndefinedOrNull(result)) {
|
||||
throw new Error(nls.localize('commandVariable.noStringType', "Cannot substitute command variable '{0}' because command did not return a result of type string.", commandId));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof result === 'string') {
|
||||
variableValues[variable] = result;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return Promise.reject(new Error(nls.localize('undefinedInputVariable', "Undefined input variable {0} encountered. Remove or define {0} to continue.", commandVariable)));
|
||||
|
||||
return variableValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all variables in object using cmdVar and pushes them into commands.
|
||||
* @param cmdVar Regex to use for finding variables.
|
||||
* Recursively finds all command or input variables in object and pushes them into variables.
|
||||
* @param object object is searched for variables.
|
||||
* @param commands All found variables are returned in commands.
|
||||
* @param variables All found variables are returned in variables.
|
||||
*/
|
||||
private findVariables(cmdVar: RegExp, object: any, commands: string[]) {
|
||||
if (!object) {
|
||||
return;
|
||||
} else if (typeof object === 'string') {
|
||||
private findVariables(object: any, variables: string[]) {
|
||||
if (typeof object === 'string') {
|
||||
let matches;
|
||||
while ((matches = cmdVar.exec(object)) !== null) {
|
||||
if (matches.length === 2) {
|
||||
while ((matches = ConfigurationResolverService.INPUT_OR_COMMAND_VARIABLES_PATTERN.exec(object)) !== null) {
|
||||
if (matches.length === 4) {
|
||||
const command = matches[1];
|
||||
if (commands.indexOf(command) < 0) {
|
||||
commands.push(command);
|
||||
if (variables.indexOf(command) < 0) {
|
||||
variables.push(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (Types.isArray(object)) {
|
||||
object.forEach(value => {
|
||||
this.findVariables(cmdVar, value, commands);
|
||||
this.findVariables(value, variables);
|
||||
});
|
||||
} else {
|
||||
} else if (object) {
|
||||
Object.keys(object).forEach(key => {
|
||||
const value = object[key];
|
||||
this.findVariables(cmdVar, value, commands);
|
||||
this.findVariables(value, variables);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of inputs into an actaul array of typed, ConfiguredInputs.
|
||||
* @param object Array of something that should look like inputs.
|
||||
* Takes the provided input info and shows the quick pick so the user can provide the value for the input
|
||||
* @param variable Name of the input variable.
|
||||
* @param inputInfos Information about each possible input variable.
|
||||
*/
|
||||
private parseConfigurationInputs(object: any[]): ConfiguredInput[] | undefined {
|
||||
let inputs = new Array<ConfiguredInput>();
|
||||
if (object) {
|
||||
object.forEach(item => {
|
||||
if (Types.isString(item.id) && Types.isString(item.description) && Types.isString(item.type)) {
|
||||
let type: ConfiguredInputType;
|
||||
switch (item.type) {
|
||||
case 'promptString': type = ConfiguredInputType.PromptString; break;
|
||||
case 'pickString': type = ConfiguredInputType.PickString; break;
|
||||
default: {
|
||||
throw new Error(nls.localize('unknownInputTypeProvided', "Input '{0}' can only be of type 'promptString' or 'pickString'.", item.id));
|
||||
}
|
||||
}
|
||||
let options: string[];
|
||||
if (type === ConfiguredInputType.PickString) {
|
||||
if (Types.isStringArray(item.options)) {
|
||||
options = item.options;
|
||||
} else {
|
||||
throw new Error(nls.localize('pickStringRequiresOptions', "Input '{0}' is of type 'pickString' and must include 'options'.", item.id));
|
||||
}
|
||||
}
|
||||
inputs.push({ id: item.id, description: item.description, type, default: item.default, options });
|
||||
}
|
||||
});
|
||||
}
|
||||
private showUserInput(variable: string, inputInfos: ConfiguredInput[]): Promise<string> {
|
||||
|
||||
return inputs;
|
||||
// find info for the given input variable
|
||||
const info = inputInfos.filter(item => item.id === variable).pop();
|
||||
if (info) {
|
||||
|
||||
const missingAttribute = (attrName: string) => {
|
||||
throw new Error(nls.localize('inputVariable.missingAttribute', "Input variable '{0}' is of type '{1}' and must include '{2}'.", variable, info.type, attrName));
|
||||
};
|
||||
|
||||
switch (info.type) {
|
||||
|
||||
case 'promptString': {
|
||||
if (!Types.isString(info.description)) {
|
||||
missingAttribute('description');
|
||||
}
|
||||
const inputOptions: IInputOptions = { prompt: info.description };
|
||||
if (info.default) {
|
||||
inputOptions.value = info.default;
|
||||
}
|
||||
return this.quickInputService.input(inputOptions).then(resolvedInput => {
|
||||
return resolvedInput ? resolvedInput : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
case 'pickString': {
|
||||
if (!Types.isString(info.description)) {
|
||||
missingAttribute('description');
|
||||
}
|
||||
if (!Types.isStringArray(info.options)) {
|
||||
missingAttribute('options');
|
||||
}
|
||||
const picks = new Array<IQuickPickItem>();
|
||||
info.options.forEach(pickOption => {
|
||||
const item: IQuickPickItem = { label: pickOption };
|
||||
if (pickOption === info.default) {
|
||||
item.description = nls.localize('inputVariable.defaultInputValue', "Default");
|
||||
picks.unshift(item);
|
||||
} else {
|
||||
picks.push(item);
|
||||
}
|
||||
});
|
||||
const pickOptions: IPickOptions<IQuickPickItem> = { placeHolder: info.description };
|
||||
return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => {
|
||||
return resolvedInput ? resolvedInput.label : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
case 'command': {
|
||||
if (!Types.isString(info.command)) {
|
||||
missingAttribute('command');
|
||||
}
|
||||
return this.commandService.executeCommand<string>(info.command, info.args).then(result => {
|
||||
if (typeof result === 'string' || Types.isUndefinedOrNull(result)) {
|
||||
return result;
|
||||
}
|
||||
throw new Error(nls.localize('inputVariable.command.noStringType', "Cannot substitute input variable '{0}' because command '{1}' did not return a result of type string.", variable, info.command));
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(nls.localize('inputVariable.unknownType', "Input variable '{0}' can only be of type 'promptString', 'pickString', or 'command'.", variable));
|
||||
}
|
||||
}
|
||||
return Promise.reject(new Error(nls.localize('inputVariable.undefinedVariable', "Undefined input variable '{0}' encountered. Remove or define '{0}' to continue.", variable)));
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import { localize } from 'vs/nls';
|
||||
import { URI as uri } from 'vs/base/common/uri';
|
||||
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
export interface IVariableResolveContext {
|
||||
getFolderUri(folderName: string): uri | undefined;
|
||||
@@ -83,11 +82,11 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
|
||||
return { newConfig, resolvedVariables };
|
||||
}
|
||||
|
||||
public resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any): TPromise<any> {
|
||||
public resolveWithInteractionReplace(folder: IWorkspaceFolder, config: any): Promise<any> {
|
||||
throw new Error('resolveWithInteractionReplace not implemented.');
|
||||
}
|
||||
|
||||
public resolveWithInteraction(folder: IWorkspaceFolder, config: any): TPromise<any> {
|
||||
public resolveWithInteraction(folder: IWorkspaceFolder, config: any): Promise<any> {
|
||||
throw new Error('resolveWithInteraction not implemented.');
|
||||
}
|
||||
|
||||
|
||||
@@ -416,6 +416,33 @@ suite('Configuration Resolver Service', () => {
|
||||
assert.equal(0, mockCommandService.callCount);
|
||||
});
|
||||
});
|
||||
test('a single command input variable', () => {
|
||||
|
||||
const configuration = {
|
||||
'name': 'Attach to Process',
|
||||
'type': 'node',
|
||||
'request': 'attach',
|
||||
'processId': '${input:input4}',
|
||||
'port': 5858,
|
||||
'sourceMaps': false,
|
||||
'outDir': null
|
||||
};
|
||||
|
||||
return configurationResolverService.resolveWithInteractionReplace(workspace, configuration, 'tasks').then(result => {
|
||||
|
||||
assert.deepEqual(result, {
|
||||
'name': 'Attach to Process',
|
||||
'type': 'node',
|
||||
'request': 'attach',
|
||||
'processId': 'arg for command',
|
||||
'port': 5858,
|
||||
'sourceMaps': false,
|
||||
'outDir': null
|
||||
});
|
||||
|
||||
assert.equal(1, mockCommandService.callCount);
|
||||
});
|
||||
});
|
||||
test('several input variables and command', () => {
|
||||
|
||||
const configuration = {
|
||||
@@ -423,6 +450,7 @@ suite('Configuration Resolver Service', () => {
|
||||
'type': '${command:command1}',
|
||||
'request': '${input:input1}',
|
||||
'processId': '${input:input2}',
|
||||
'command': '${input:input4}',
|
||||
'port': 5858,
|
||||
'sourceMaps': false,
|
||||
'outDir': null
|
||||
@@ -435,12 +463,13 @@ suite('Configuration Resolver Service', () => {
|
||||
'type': 'command1-result',
|
||||
'request': 'resolvedEnterinput1',
|
||||
'processId': 'selectedPick',
|
||||
'command': 'arg for command',
|
||||
'port': 5858,
|
||||
'sourceMaps': false,
|
||||
'outDir': null
|
||||
});
|
||||
|
||||
assert.equal(1, mockCommandService.callCount);
|
||||
assert.equal(2, mockCommandService.callCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -450,7 +479,7 @@ class MockConfigurationService implements IConfigurationService {
|
||||
public _serviceBrand: any;
|
||||
public serviceId = IConfigurationService;
|
||||
public constructor(private configuration: any = {}) { }
|
||||
public inspect<T>(key: string, overrides?: IConfigurationOverrides): any { return { value: getConfigurationValue<T>(this.getValue(), key), default: getConfigurationValue<T>(this.getValue(), key), user: getConfigurationValue<T>(this.getValue(), key), workspaceFolder: void 0, folder: void 0 }; }
|
||||
public inspect<T>(key: string, overrides?: IConfigurationOverrides): any { return { value: getConfigurationValue<T>(this.getValue(), key), default: getConfigurationValue<T>(this.getValue(), key), user: getConfigurationValue<T>(this.getValue(), key), workspaceFolder: undefined, folder: undefined }; }
|
||||
public keys() { return { default: [], user: [], workspace: [], workspaceFolder: [] }; }
|
||||
public getValue(): any;
|
||||
public getValue(value: string): any;
|
||||
@@ -495,9 +524,9 @@ class MockCommandService implements ICommandService {
|
||||
class MockQuickInputService implements IQuickInputService {
|
||||
_serviceBrand: any;
|
||||
|
||||
public pick<T extends IQuickPickItem>(picks: Thenable<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: true }, token?: CancellationToken): Promise<T[]>;
|
||||
public pick<T extends IQuickPickItem>(picks: Thenable<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: false }, token?: CancellationToken): Promise<T>;
|
||||
public pick<T extends IQuickPickItem>(picks: Thenable<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: Omit<IPickOptions<T>, 'canPickMany'>, token?: CancellationToken): Promise<T> {
|
||||
public pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: true }, token?: CancellationToken): Promise<T[]>;
|
||||
public pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: IPickOptions<T> & { canPickMany: false }, token?: CancellationToken): Promise<T>;
|
||||
public pick<T extends IQuickPickItem>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: Omit<IPickOptions<T>, 'canPickMany'>, token?: CancellationToken): Promise<T> {
|
||||
if (Types.isArray(picks)) {
|
||||
return Promise.resolve(<T>{ label: 'selectedPick', description: 'pick description' });
|
||||
} else {
|
||||
@@ -568,6 +597,14 @@ class MockInputsConfigurationService extends TestConfigurationService {
|
||||
type: 'promptString',
|
||||
description: 'Enterinput3',
|
||||
default: 'default input3'
|
||||
},
|
||||
{
|
||||
id: 'input4',
|
||||
type: 'command',
|
||||
command: 'command1',
|
||||
args: {
|
||||
value: 'arg for command'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -27,9 +27,9 @@ export class ContextMenuService extends Disposable implements IContextMenuServic
|
||||
get onDidContextMenu(): Event<void> { return this._onDidContextMenu.event; }
|
||||
|
||||
constructor(
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@ITelemetryService private telemetryService: ITelemetryService,
|
||||
@IKeybindingService private keybindingService: IKeybindingService
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -39,7 +39,7 @@ export class ContextMenuService extends Disposable implements IContextMenuServic
|
||||
if (actions.length) {
|
||||
const onHide = once(() => {
|
||||
if (delegate.onHide) {
|
||||
delegate.onHide(undefined);
|
||||
delegate.onHide(false);
|
||||
}
|
||||
|
||||
this._onDidContextMenu.fire();
|
||||
@@ -67,13 +67,13 @@ export class ContextMenuService extends Disposable implements IContextMenuServic
|
||||
popup(menu, {
|
||||
x: Math.floor(x),
|
||||
y: Math.floor(y),
|
||||
positioningItem: delegate.autoSelectFirstItem ? 0 : void 0,
|
||||
positioningItem: delegate.autoSelectFirstItem ? 0 : undefined,
|
||||
onHide: () => onHide()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private createMenu(delegate: IContextMenuDelegate, entries: (IAction | ContextSubMenu)[], onHide: () => void): IContextMenuItem[] {
|
||||
private createMenu(delegate: IContextMenuDelegate, entries: Array<IAction | ContextSubMenu>, onHide: () => void): IContextMenuItem[] {
|
||||
const actionRunner = delegate.actionRunner || new ActionRunner();
|
||||
|
||||
return entries.map(entry => this.createMenuItem(delegate, entry, actionRunner, onHide));
|
||||
@@ -99,7 +99,7 @@ export class ContextMenuService extends Disposable implements IContextMenuServic
|
||||
const item: IContextMenuItem = {
|
||||
label: unmnemonicLabel(entry.label),
|
||||
checked: !!entry.checked || !!entry.radio,
|
||||
type: !!entry.checked ? 'checkbox' : !!entry.radio ? 'radio' : void 0,
|
||||
type: !!entry.checked ? 'checkbox' : !!entry.radio ? 'radio' : undefined,
|
||||
enabled: !!entry.enabled,
|
||||
click: event => {
|
||||
|
||||
@@ -142,6 +142,6 @@ export class ContextMenuService extends Disposable implements IContextMenuServic
|
||||
const context = delegate.getActionsContext ? delegate.getActionsContext(event) : event;
|
||||
const res = actionRunner.run(actionToRun, context) || Promise.resolve(null);
|
||||
|
||||
res.then(null, e => this.notificationService.error(e));
|
||||
res.then(undefined, e => this.notificationService.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ export class CrashReporterService implements ICrashReporterService {
|
||||
private isEnabled: boolean;
|
||||
|
||||
constructor(
|
||||
@ITelemetryService private telemetryService: ITelemetryService,
|
||||
@IWindowsService private windowsService: IWindowsService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IWindowsService private readonly windowsService: IWindowsService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
const config = configurationService.getValue<ICrashReporterConfig>(TELEMETRY_SECTION_ID);
|
||||
@@ -121,6 +121,6 @@ export class CrashReporterService implements ICrashReporterService {
|
||||
return childProcessOptions;
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export interface IDecoration {
|
||||
export interface IDecorationsProvider {
|
||||
readonly label: string;
|
||||
readonly onDidChange: Event<URI[]>;
|
||||
provideDecorations(uri: URI, token: CancellationToken): IDecorationData | Thenable<IDecorationData> | undefined;
|
||||
provideDecorations(uri: URI, token: CancellationToken): IDecorationData | Promise<IDecorationData> | undefined;
|
||||
}
|
||||
|
||||
export interface IResourceDecorationChangeEvent {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Event, Emitter, debounceEvent, anyEvent } from 'vs/base/common/event';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDecorationsService, IDecoration, IResourceDecorationChangeEvent, IDecorationsProvider, IDecorationData } from './decorations';
|
||||
import { TernarySearchTree } from 'vs/base/common/map';
|
||||
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
@@ -222,7 +222,7 @@ class FileDecorationChangeEvent implements IResourceDecorationChangeEvent {
|
||||
class DecorationDataRequest {
|
||||
constructor(
|
||||
readonly source: CancellationTokenSource,
|
||||
readonly thenable: Thenable<void>,
|
||||
readonly thenable: Promise<void>,
|
||||
) { }
|
||||
}
|
||||
|
||||
@@ -339,13 +339,13 @@ export class FileDecorationsService implements IDecorationsService {
|
||||
|
||||
private readonly _data = new LinkedList<DecorationProviderWrapper>();
|
||||
private readonly _onDidChangeDecorationsDelayed = new Emitter<URI | URI[]>();
|
||||
private readonly _onDidChangeDecorations = new Emitter<IResourceDecorationChangeEvent>({ leakWarningThreshold: 500 });
|
||||
private readonly _onDidChangeDecorations = new Emitter<IResourceDecorationChangeEvent>();
|
||||
private readonly _decorationStyles: DecorationStyles;
|
||||
private readonly _disposables: IDisposable[];
|
||||
|
||||
readonly onDidChangeDecorations: Event<IResourceDecorationChangeEvent> = anyEvent(
|
||||
readonly onDidChangeDecorations: Event<IResourceDecorationChangeEvent> = Event.any(
|
||||
this._onDidChangeDecorations.event,
|
||||
debounceEvent<URI | URI[], FileDecorationChangeEvent>(
|
||||
Event.debounce<URI | URI[], FileDecorationChangeEvent>(
|
||||
this._onDidChangeDecorationsDelayed.event,
|
||||
FileDecorationChangeEvent.debouncer,
|
||||
undefined, undefined, 500
|
||||
|
||||
@@ -7,7 +7,7 @@ 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, Emitter } from 'vs/base/common/event';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
@@ -46,11 +46,11 @@ suite('DecorationsService', function () {
|
||||
assert.equal(callCounter, 1);
|
||||
|
||||
// event when result is computed
|
||||
return toPromise(service.onDidChangeDecorations).then(e => {
|
||||
return Event.toPromise(service.onDidChangeDecorations).then(e => {
|
||||
assert.equal(e.affectsResource(uri), true);
|
||||
|
||||
// sync result
|
||||
assert.deepEqual(service.getDecoration(uri, false).tooltip, 'T');
|
||||
assert.deepEqual(service.getDecoration(uri, false)!.tooltip, 'T');
|
||||
assert.equal(callCounter, 1);
|
||||
});
|
||||
});
|
||||
@@ -70,7 +70,7 @@ suite('DecorationsService', function () {
|
||||
});
|
||||
|
||||
// trigger -> sync
|
||||
assert.deepEqual(service.getDecoration(uri, false).tooltip, 'Z');
|
||||
assert.deepEqual(service.getDecoration(uri, false)!.tooltip, 'Z');
|
||||
assert.equal(callCounter, 1);
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ suite('DecorationsService', function () {
|
||||
});
|
||||
|
||||
// trigger -> sync
|
||||
assert.deepEqual(service.getDecoration(uri, false).tooltip, 'J');
|
||||
assert.deepEqual(service.getDecoration(uri, false)!.tooltip, 'J');
|
||||
assert.equal(callCounter, 1);
|
||||
|
||||
// un-register -> ensure good event
|
||||
@@ -121,10 +121,10 @@ suite('DecorationsService', function () {
|
||||
|
||||
let childUri = URI.parse('file:///some/path/some/file.txt');
|
||||
|
||||
let deco = service.getDecoration(childUri, false);
|
||||
let deco = service.getDecoration(childUri, false)!;
|
||||
assert.equal(deco.tooltip, '.txt');
|
||||
|
||||
deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true);
|
||||
deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true)!;
|
||||
assert.equal(deco, undefined);
|
||||
reg.dispose();
|
||||
|
||||
@@ -139,10 +139,10 @@ suite('DecorationsService', function () {
|
||||
}
|
||||
});
|
||||
|
||||
deco = service.getDecoration(childUri, false);
|
||||
deco = service.getDecoration(childUri, false)!;
|
||||
assert.equal(deco.tooltip, '.txt.bubble');
|
||||
|
||||
deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true);
|
||||
deco = service.getDecoration(childUri.with({ path: 'some/path/' }), true)!;
|
||||
assert.equal(typeof deco.tooltip, 'string');
|
||||
});
|
||||
|
||||
@@ -152,7 +152,7 @@ suite('DecorationsService', function () {
|
||||
let deco = service.getDecoration(someUri, false);
|
||||
assert.equal(deco, undefined);
|
||||
|
||||
deco = service.getDecoration(someUri, false, { tooltip: 'Overwrite' });
|
||||
deco = service.getDecoration(someUri, false, { tooltip: 'Overwrite' })!;
|
||||
assert.equal(deco.tooltip, 'Overwrite');
|
||||
|
||||
let reg = service.registerDecorationsProvider({
|
||||
@@ -163,10 +163,10 @@ suite('DecorationsService', function () {
|
||||
}
|
||||
});
|
||||
|
||||
deco = service.getDecoration(someUri, false);
|
||||
deco = service.getDecoration(someUri, false)!;
|
||||
assert.equal(deco.tooltip, 'FromMe');
|
||||
|
||||
deco = service.getDecoration(someUri, false, { source: 'foo', tooltip: 'O' });
|
||||
deco = service.getDecoration(someUri, false, { source: 'foo', tooltip: 'O' })!;
|
||||
assert.equal(deco.tooltip, 'O');
|
||||
|
||||
reg.dispose();
|
||||
@@ -232,7 +232,7 @@ suite('DecorationsService', function () {
|
||||
let data1 = service.getDecoration(URI.parse('a:b/'), true);
|
||||
assert.ok(!data1);
|
||||
|
||||
let data2 = service.getDecoration(URI.parse('a:b/c.hello'), false);
|
||||
let data2 = service.getDecoration(URI.parse('a:b/c.hello'), false)!;
|
||||
assert.ok(data2.tooltip);
|
||||
|
||||
let data3 = service.getDecoration(URI.parse('a:b/'), true);
|
||||
@@ -259,19 +259,19 @@ suite('DecorationsService', function () {
|
||||
|
||||
let uri = URI.parse('foo:/folder/file.ts');
|
||||
let uri2 = URI.parse('foo:/folder/');
|
||||
let data = service.getDecoration(uri, true);
|
||||
let data = service.getDecoration(uri, true)!;
|
||||
assert.equal(data.tooltip, 'FOO');
|
||||
|
||||
data = service.getDecoration(uri2, true);
|
||||
data = service.getDecoration(uri2, true)!;
|
||||
assert.ok(data.tooltip); // emphazied items...
|
||||
|
||||
gone = true;
|
||||
emitter.fire([uri]);
|
||||
|
||||
data = service.getDecoration(uri, true);
|
||||
data = service.getDecoration(uri, true)!;
|
||||
assert.equal(data, undefined);
|
||||
|
||||
data = service.getDecoration(uri2, true);
|
||||
data = service.getDecoration(uri2, true)!;
|
||||
assert.equal(data, undefined);
|
||||
|
||||
reg.dispose();
|
||||
@@ -294,10 +294,10 @@ suite('DecorationsService', function () {
|
||||
|
||||
let uri = URI.parse('foo:/folder/file.ts');
|
||||
let uri2 = URI.parse('foo:/folder/');
|
||||
let data = service.getDecoration(uri, true);
|
||||
let data = service.getDecoration(uri, true)!;
|
||||
assert.equal(data.tooltip, 'FOO');
|
||||
|
||||
data = service.getDecoration(uri2, true);
|
||||
data = service.getDecoration(uri2, true)!;
|
||||
assert.ok(data.tooltip); // emphazied items...
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
@@ -39,11 +39,11 @@ export class DialogService implements IDialogService {
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IWindowService private windowService: IWindowService,
|
||||
@ILogService private logService: ILogService
|
||||
@IWindowService private readonly windowService: IWindowService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) { }
|
||||
|
||||
confirm(confirmation: IConfirmation): Thenable<IConfirmationResult> {
|
||||
confirm(confirmation: IConfirmation): Promise<IConfirmationResult> {
|
||||
this.logService.trace('DialogService#confirm', confirmation.message);
|
||||
|
||||
const { options, buttonIndexMap } = this.massageMessageBoxOptions(this.getConfirmOptions(confirmation));
|
||||
@@ -93,23 +93,23 @@ export class DialogService implements IDialogService {
|
||||
return opts;
|
||||
}
|
||||
|
||||
show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Thenable<number> {
|
||||
show(severity: Severity, message: string, buttons: string[], dialogOptions?: IDialogOptions): Promise<number> {
|
||||
this.logService.trace('DialogService#show', message);
|
||||
|
||||
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
|
||||
cancelId: dialogOptions ? dialogOptions.cancelId : undefined,
|
||||
detail: dialogOptions ? dialogOptions.detail : undefined
|
||||
});
|
||||
|
||||
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 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
|
||||
@@ -157,45 +157,46 @@ export class FileDialogService implements IFileDialogService {
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IWindowService private windowService: IWindowService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IHistoryService private historyService: IHistoryService,
|
||||
@IEnvironmentService private environmentService: IEnvironmentService
|
||||
@IWindowService private readonly windowService: IWindowService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IHistoryService private readonly historyService: IHistoryService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService
|
||||
) { }
|
||||
|
||||
public defaultFilePath(schemeFilter: string): URI {
|
||||
let candidate: URI;
|
||||
defaultFilePath(schemeFilter: string): URI | undefined {
|
||||
|
||||
// Check for last active file first...
|
||||
candidate = this.historyService.getLastActiveFile(schemeFilter);
|
||||
let candidate = this.historyService.getLastActiveFile(schemeFilter);
|
||||
|
||||
// ...then for last active file root
|
||||
if (!candidate) {
|
||||
candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
|
||||
}
|
||||
|
||||
return candidate ? resources.dirname(candidate) : void 0;
|
||||
return candidate && resources.dirname(candidate) || undefined;
|
||||
}
|
||||
|
||||
public defaultFolderPath(schemeFilter: string): URI {
|
||||
let candidate: URI;
|
||||
defaultFolderPath(schemeFilter: string): URI | undefined {
|
||||
|
||||
// Check for last active file root first...
|
||||
candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
|
||||
let candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter);
|
||||
|
||||
// ...then for last active file
|
||||
if (!candidate) {
|
||||
candidate = this.historyService.getLastActiveFile(schemeFilter);
|
||||
}
|
||||
|
||||
return candidate ? resources.dirname(candidate) : void 0;
|
||||
return candidate && resources.dirname(candidate) || undefined;
|
||||
}
|
||||
|
||||
public defaultWorkspacePath(schemeFilter: string): URI {
|
||||
defaultWorkspacePath(schemeFilter: string): URI | undefined {
|
||||
|
||||
// Check for current workspace config file first...
|
||||
if (schemeFilter === Schemas.file && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && !isUntitledWorkspace(this.contextService.getWorkspace().configuration.fsPath, this.environmentService)) {
|
||||
return resources.dirname(this.contextService.getWorkspace().configuration);
|
||||
if (schemeFilter === Schemas.file && this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
|
||||
const configuration = this.contextService.getWorkspace().configuration;
|
||||
if (configuration && !isUntitledWorkspace(configuration.fsPath, this.environmentService)) {
|
||||
return resources.dirname(configuration) || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// ...then fallback to default folder path
|
||||
@@ -212,36 +213,39 @@ export class FileDialogService implements IFileDialogService {
|
||||
};
|
||||
}
|
||||
|
||||
public pickFileFolderAndOpen(options: IPickAndOpenOptions): Thenable<any> {
|
||||
let defaultUri = options.defaultUri;
|
||||
pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
const defaultUri = options.defaultUri;
|
||||
if (!defaultUri) {
|
||||
options.defaultUri = this.defaultFilePath(Schemas.file);
|
||||
}
|
||||
return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
|
||||
return this.windowService.pickFileFolderAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
}
|
||||
|
||||
public pickFileAndOpen(options: IPickAndOpenOptions): Thenable<any> {
|
||||
let defaultUri = options.defaultUri;
|
||||
pickFileAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
const defaultUri = options.defaultUri;
|
||||
if (!defaultUri) {
|
||||
options.defaultUri = this.defaultFilePath(Schemas.file);
|
||||
}
|
||||
|
||||
return this.windowService.pickFileAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
}
|
||||
|
||||
public pickFolderAndOpen(options: IPickAndOpenOptions): Thenable<any> {
|
||||
let defaultUri = options.defaultUri;
|
||||
pickFolderAndOpen(options: IPickAndOpenOptions): Promise<any> {
|
||||
const defaultUri = options.defaultUri;
|
||||
if (!defaultUri) {
|
||||
options.defaultUri = this.defaultFolderPath(Schemas.file);
|
||||
}
|
||||
|
||||
return this.windowService.pickFolderAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
}
|
||||
|
||||
public pickWorkspaceAndOpen(options: IPickAndOpenOptions): Thenable<void> {
|
||||
let defaultUri = options.defaultUri;
|
||||
pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void> {
|
||||
const defaultUri = options.defaultUri;
|
||||
if (!defaultUri) {
|
||||
options.defaultUri = this.defaultWorkspacePath(Schemas.file);
|
||||
}
|
||||
|
||||
return this.windowService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options));
|
||||
}
|
||||
|
||||
@@ -254,20 +258,22 @@ export class FileDialogService implements IFileDialogService {
|
||||
};
|
||||
}
|
||||
|
||||
public showSaveDialog(options: ISaveDialogOptions): Thenable<URI> {
|
||||
showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
|
||||
const defaultUri = options.defaultUri;
|
||||
if (defaultUri && defaultUri.scheme !== Schemas.file) {
|
||||
return Promise.reject(new Error('Not supported - Save-dialogs can only be opened on `file`-uris.'));
|
||||
}
|
||||
|
||||
return this.windowService.showSaveDialog(this.toNativeSaveDialogOptions(options)).then(result => {
|
||||
if (result) {
|
||||
return URI.file(result);
|
||||
}
|
||||
return void 0;
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public showOpenDialog(options: IOpenDialogOptions): Thenable<URI[] | undefined> {
|
||||
showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
|
||||
const defaultUri = options.defaultUri;
|
||||
if (defaultUri && defaultUri.scheme !== Schemas.file) {
|
||||
return Promise.reject(new Error('Not supported - Open-dialogs can only be opened on `file`-uris.'));
|
||||
@@ -280,17 +286,22 @@ export class FileDialogService implements IFileDialogService {
|
||||
filters: options.filters,
|
||||
properties: []
|
||||
};
|
||||
newOptions.properties.push('createDirectory');
|
||||
|
||||
newOptions.properties!.push('createDirectory');
|
||||
|
||||
if (options.canSelectFiles) {
|
||||
newOptions.properties.push('openFile');
|
||||
newOptions.properties!.push('openFile');
|
||||
}
|
||||
|
||||
if (options.canSelectFolders) {
|
||||
newOptions.properties.push('openDirectory');
|
||||
newOptions.properties!.push('openDirectory');
|
||||
}
|
||||
|
||||
if (options.canSelectMany) {
|
||||
newOptions.properties.push('multiSelections');
|
||||
newOptions.properties!.push('multiSelections');
|
||||
}
|
||||
return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : void 0);
|
||||
|
||||
return this.windowService.showOpenDialog(newOptions).then(result => result ? result.map(URI.file) : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,11 @@ import { ResourceMap } from 'vs/base/common/map';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { Event, once, Emitter } from 'vs/base/common/event';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { basename } from 'vs/base/common/paths';
|
||||
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
|
||||
import { localize } from 'vs/nls';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
import { IResourceEditor, ACTIVE_GROUP_TYPE, SIDE_GROUP_TYPE, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
@@ -62,12 +61,12 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
private lastActiveGroupId: GroupIdentifier;
|
||||
|
||||
constructor(
|
||||
@IEditorGroupsService private editorGroupService: EditorGroupsServiceImpl,
|
||||
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@ILabelService private labelService: ILabelService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@IEditorGroupsService private readonly editorGroupService: EditorGroupsServiceImpl,
|
||||
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ILabelService private readonly labelService: ILabelService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -141,14 +140,13 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
this._onDidOpenEditorFail.fire({ editor, groupId: group.id });
|
||||
}));
|
||||
|
||||
once(group.onWillDispose)(() => {
|
||||
Event.once(group.onWillDispose)(() => {
|
||||
dispose(groupDisposeables);
|
||||
});
|
||||
}
|
||||
|
||||
private onGroupWillOpenEditor(group: IEditorGroup, event: IEditorOpeningEvent): void {
|
||||
for (let i = 0; i < this.openEditorHandlers.length; i++) {
|
||||
const handler = this.openEditorHandlers[i];
|
||||
for (const handler of this.openEditorHandlers) {
|
||||
const result = handler(event.editor, event.options, group);
|
||||
if (result && result.override) {
|
||||
event.prevent((() => result.override));
|
||||
@@ -160,7 +158,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
get activeControl(): IEditor {
|
||||
const activeGroup = this.editorGroupService.activeGroup;
|
||||
|
||||
return activeGroup ? activeGroup.activeControl : void 0;
|
||||
return activeGroup ? activeGroup.activeControl : undefined;
|
||||
}
|
||||
|
||||
get activeTextEditorWidget(): ICodeEditor | IDiffEditor {
|
||||
@@ -172,7 +170,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get editors(): IEditorInput[] {
|
||||
@@ -187,14 +185,14 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
get activeEditor(): IEditorInput {
|
||||
const activeGroup = this.editorGroupService.activeGroup;
|
||||
|
||||
return activeGroup ? activeGroup.activeEditor : void 0;
|
||||
return activeGroup ? activeGroup.activeEditor : undefined;
|
||||
}
|
||||
|
||||
get visibleControls(): IEditor[] {
|
||||
return coalesce(this.editorGroupService.groups.map(group => group.activeControl));
|
||||
}
|
||||
|
||||
get visibleTextEditorWidgets(): (ICodeEditor | IDiffEditor)[] {
|
||||
get visibleTextEditorWidgets(): Array<ICodeEditor | IDiffEditor> {
|
||||
return this.visibleControls.map(control => control.getControl() as ICodeEditor | IDiffEditor).filter(widget => isCodeEditor(widget) || isDiffEditor(widget));
|
||||
}
|
||||
|
||||
@@ -219,11 +217,11 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
//#region openEditor()
|
||||
|
||||
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor>;
|
||||
openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextEditor>;
|
||||
openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextDiffEditor>;
|
||||
openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextSideBySideEditor>;
|
||||
openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, group?: GroupIdentifier): TPromise<IEditor> {
|
||||
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor>;
|
||||
openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextEditor>;
|
||||
openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextDiffEditor>;
|
||||
openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextSideBySideEditor>;
|
||||
openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE, group?: GroupIdentifier): Promise<IEditor> {
|
||||
|
||||
// Typed Editor Support
|
||||
if (editor instanceof EditorInput) {
|
||||
@@ -242,11 +240,11 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
return this.doOpenEditor(targetGroup, typedInput, editorOptions);
|
||||
}
|
||||
|
||||
return TPromise.wrap<IEditor>(null);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
protected doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): TPromise<IEditor> {
|
||||
return group.openEditor(editor, options).then(() => group.activeControl);
|
||||
protected doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise<IEditor> {
|
||||
return group.openEditor(editor, options);
|
||||
}
|
||||
|
||||
private findTargetGroup(input: IEditorInput, options?: IEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): IEditorGroup {
|
||||
@@ -273,8 +271,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
// Respect option to reveal an editor if it is already visible in any group
|
||||
if (options && options.revealIfVisible) {
|
||||
for (let i = 0; i < groupsByLastActive.length; i++) {
|
||||
const group = groupsByLastActive[i];
|
||||
for (const group of groupsByLastActive) {
|
||||
if (input.matches(group.activeEditor)) {
|
||||
targetGroup = group;
|
||||
break;
|
||||
@@ -284,8 +281,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
// Respect option to reveal an editor if it is open (not necessarily visible)
|
||||
if ((options && options.revealIfOpened) || this.configurationService.getValue<boolean>('workbench.editor.revealIfOpen')) {
|
||||
for (let i = 0; i < groupsByLastActive.length; i++) {
|
||||
const group = groupsByLastActive[i];
|
||||
for (const group of groupsByLastActive) {
|
||||
if (group.isOpened(input)) {
|
||||
targetGroup = group;
|
||||
break;
|
||||
@@ -330,9 +326,9 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
//#region openEditors()
|
||||
|
||||
openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor[]>;
|
||||
openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor[]>;
|
||||
openEditors(editors: (IEditorInputWithOptions | IResourceEditor)[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor[]> {
|
||||
openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor[]>;
|
||||
openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor[]>;
|
||||
openEditors(editors: Array<IEditorInputWithOptions | IResourceEditor>, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor[]> {
|
||||
|
||||
// Convert to typed editors and options
|
||||
const typedEditors: IEditorInputWithOptions[] = [];
|
||||
@@ -363,12 +359,12 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
// Open in targets
|
||||
const result: TPromise<IEditor>[] = [];
|
||||
const result: Promise<IEditor>[] = [];
|
||||
mapGroupToEditors.forEach((editorsWithOptions, group) => {
|
||||
result.push((group.openEditors(editorsWithOptions)).then(() => group.activeControl));
|
||||
result.push(group.openEditors(editorsWithOptions));
|
||||
});
|
||||
|
||||
return TPromise.join(result);
|
||||
return Promise.all(result);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@@ -391,7 +387,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
if (!(editor instanceof EditorInput)) {
|
||||
const resourceInput = editor as IResourceInput | IUntitledResourceInput;
|
||||
if (!resourceInput.resource) {
|
||||
return void 0; // we need a resource at least
|
||||
return undefined; // we need a resource at least
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,8 +401,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
// For each editor group
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
for (const group of groups) {
|
||||
|
||||
// Typed editor
|
||||
if (editor instanceof EditorInput) {
|
||||
@@ -417,8 +412,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
|
||||
// Resource editor
|
||||
else {
|
||||
for (let j = 0; j < group.editors.length; j++) {
|
||||
const editorInGroup = group.editors[j];
|
||||
for (const editorInGroup of group.editors) {
|
||||
const resource = toResource(editorInGroup, { supportSideBySide: true });
|
||||
if (!resource) {
|
||||
continue; // need a resource to compare with
|
||||
@@ -432,16 +426,16 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region replaceEditors()
|
||||
|
||||
replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): TPromise<void>;
|
||||
replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): TPromise<void>;
|
||||
replaceEditors(editors: (IEditorReplacement | IResourceEditorReplacement)[], group: IEditorGroup | GroupIdentifier): TPromise<void> {
|
||||
replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
|
||||
replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
|
||||
replaceEditors(editors: Array<IEditorReplacement | IResourceEditorReplacement>, group: IEditorGroup | GroupIdentifier): Promise<void> {
|
||||
const typedEditors: IEditorReplacement[] = [];
|
||||
|
||||
editors.forEach(replaceEditorArg => {
|
||||
@@ -584,7 +578,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
EditorService.CACHE.set(resource, input);
|
||||
once(input.onDispose)(() => {
|
||||
Event.once(input.onDispose)(() => {
|
||||
EditorService.CACHE.delete(resource);
|
||||
});
|
||||
|
||||
@@ -607,7 +601,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
|
||||
}
|
||||
|
||||
export interface IEditorOpenHandler {
|
||||
(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): TPromise<IEditor>;
|
||||
(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): Promise<IEditor>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -639,12 +633,14 @@ export class DelegatingEditorService extends EditorService {
|
||||
this.editorOpenHandler = handler;
|
||||
}
|
||||
|
||||
protected doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): TPromise<IEditor> {
|
||||
const handleOpen = this.editorOpenHandler ? this.editorOpenHandler(group, editor, options) : TPromise.as(void 0);
|
||||
protected doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise<IEditor> {
|
||||
if (!this.editorOpenHandler) {
|
||||
return super.doOpenEditor(group, editor, options);
|
||||
}
|
||||
|
||||
return handleOpen.then(control => {
|
||||
return this.editorOpenHandler(group, editor, options).then(control => {
|
||||
if (control) {
|
||||
return TPromise.as<IEditor>(control); // the opening was handled, so return early
|
||||
return control; // the opening was handled, so return early
|
||||
}
|
||||
|
||||
return super.doOpenEditor(group, editor, options);
|
||||
|
||||
@@ -9,7 +9,6 @@ import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntit
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEditor as ICodeEditor } from 'vs/editor/common/editorCommon';
|
||||
import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export const IEditorService = createDecorator<IEditorService>('editorService');
|
||||
@@ -37,7 +36,7 @@ export interface IOpenEditorOverride {
|
||||
* If defined, will prevent the opening of an editor and replace the resulting
|
||||
* promise with the provided promise for the openEditor() call.
|
||||
*/
|
||||
override?: TPromise<any>;
|
||||
override?: Promise<IEditor>;
|
||||
}
|
||||
|
||||
export interface IEditorService {
|
||||
@@ -111,11 +110,14 @@ export interface IEditorService {
|
||||
* @param group the target group. If unspecified, the editor will open in the currently
|
||||
* active group. Use `SIDE_GROUP_TYPE` to open the editor in a new editor group to the side
|
||||
* of the currently active group.
|
||||
*
|
||||
* @returns the editor that opened or NULL if the operation failed or the editor was not
|
||||
* opened to be active.
|
||||
*/
|
||||
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<IEditor>;
|
||||
openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextEditor>;
|
||||
openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextDiffEditor>;
|
||||
openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ITextSideBySideEditor>;
|
||||
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor>;
|
||||
openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextEditor>;
|
||||
openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextDiffEditor>;
|
||||
openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextSideBySideEditor>;
|
||||
|
||||
/**
|
||||
* Open editors in an editor group.
|
||||
@@ -124,9 +126,12 @@ export interface IEditorService {
|
||||
* @param group the target group. If unspecified, the editor will open in the currently
|
||||
* active group. Use `SIDE_GROUP_TYPE` to open the editor in a new editor group to the side
|
||||
* of the currently active group.
|
||||
*
|
||||
* @returns the editors that opened. The array can be empty or have less elements for editors
|
||||
* that failed to open or were instructed to open as inactive.
|
||||
*/
|
||||
openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ReadonlyArray<IEditor>>;
|
||||
openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): TPromise<ReadonlyArray<IEditor>>;
|
||||
openEditors(editors: IEditorInputWithOptions[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ReadonlyArray<IEditor>>;
|
||||
openEditors(editors: IResourceEditor[], group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ReadonlyArray<IEditor>>;
|
||||
|
||||
/**
|
||||
* Replaces editors in an editor group with the provided replacement.
|
||||
@@ -136,8 +141,8 @@ export interface IEditorService {
|
||||
* @returns a promise that is resolved when the replaced active
|
||||
* editor (if any) has finished loading.
|
||||
*/
|
||||
replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): TPromise<void>;
|
||||
replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): TPromise<void>;
|
||||
replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
|
||||
replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise<void>;
|
||||
|
||||
/**
|
||||
* Find out if the provided editor (or resource of an editor) is opened in any or
|
||||
|
||||
@@ -4,10 +4,610 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as paths from 'vs/base/common/paths';
|
||||
import { IEditorModel } from 'vs/platform/editor/common/editor';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
|
||||
import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput } from 'vs/workbench/common/editor';
|
||||
import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService';
|
||||
import { IEditorGroup, IEditorGroupsService, GroupDirection } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput';
|
||||
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
|
||||
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
|
||||
// {{SQL CARBON EDIT}} - Remove tests
|
||||
// {{SQL CARBON EDIT}} - Disable editor tests
|
||||
/*
|
||||
export class TestEditorControl extends BaseEditor {
|
||||
|
||||
constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyTestEditorForEditorService', NullTelemetryService, new TestThemeService(), new TestStorageService()); }
|
||||
|
||||
setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise<void> {
|
||||
super.setInput(input, options, token);
|
||||
|
||||
return input.resolve().then(() => undefined);
|
||||
}
|
||||
|
||||
getId(): string { return 'MyTestEditorForEditorService'; }
|
||||
layout(): void { }
|
||||
createEditor(): any { }
|
||||
}
|
||||
|
||||
export class TestEditorInput extends EditorInput implements IFileEditorInput {
|
||||
public gotDisposed: boolean;
|
||||
private fails: boolean;
|
||||
constructor(private resource: URI) { super(); }
|
||||
|
||||
getTypeId() { return 'testEditorInputForEditorService'; }
|
||||
resolve(): Promise<IEditorModel> { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); }
|
||||
matches(other: TestEditorInput): boolean { return other && other.resource && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; }
|
||||
setEncoding(encoding: string) { }
|
||||
getEncoding(): string { return null; }
|
||||
setPreferredEncoding(encoding: string) { }
|
||||
getResource(): URI { return this.resource; }
|
||||
setForceOpenAsBinary(): void { }
|
||||
setFailToOpen(): void {
|
||||
this.fails = true;
|
||||
}
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.gotDisposed = true;
|
||||
}
|
||||
}
|
||||
*/
|
||||
suite('Editor service', () => {
|
||||
/*
|
||||
function registerTestEditorInput(): void {
|
||||
Registry.as<IEditorRegistry>(Extensions.Editors).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), new SyncDescriptor(TestEditorInput));
|
||||
}
|
||||
|
||||
registerTestEditorInput();
|
||||
|
||||
test('basics', function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart, 'id', false);
|
||||
part.create(document.createElement('div'));
|
||||
part.layout(400, 300);
|
||||
|
||||
const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part]));
|
||||
|
||||
const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService);
|
||||
|
||||
const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics'));
|
||||
const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics'));
|
||||
|
||||
let activeEditorChangeEventCounter = 0;
|
||||
const activeEditorChangeListener = service.onDidActiveEditorChange(() => {
|
||||
activeEditorChangeEventCounter++;
|
||||
});
|
||||
|
||||
let visibleEditorChangeEventCounter = 0;
|
||||
const visibleEditorChangeListener = service.onDidVisibleEditorsChange(() => {
|
||||
visibleEditorChangeEventCounter++;
|
||||
});
|
||||
|
||||
let didCloseEditorListenerCounter = 0;
|
||||
const didCloseEditorListener = service.onDidCloseEditor(editor => {
|
||||
didCloseEditorListenerCounter++;
|
||||
});
|
||||
|
||||
return part.whenRestored.then(() => {
|
||||
|
||||
// Open input
|
||||
return service.openEditor(input, { pinned: true }).then(editor => {
|
||||
assert.ok(editor instanceof TestEditorControl);
|
||||
assert.equal(editor, service.activeControl);
|
||||
assert.equal(input, service.activeEditor);
|
||||
assert.equal(service.visibleControls.length, 1);
|
||||
assert.equal(service.visibleControls[0], editor);
|
||||
assert.ok(!service.activeTextEditorWidget);
|
||||
assert.equal(service.visibleTextEditorWidgets.length, 0);
|
||||
assert.equal(service.isOpen(input), true);
|
||||
assert.equal(service.getOpened({ resource: input.getResource() }), input);
|
||||
assert.equal(service.isOpen(input, part.activeGroup), true);
|
||||
assert.equal(activeEditorChangeEventCounter, 1);
|
||||
assert.equal(visibleEditorChangeEventCounter, 1);
|
||||
|
||||
// Close input
|
||||
return editor.group.closeEditor(input).then(() => {
|
||||
assert.equal(didCloseEditorListenerCounter, 1);
|
||||
assert.equal(activeEditorChangeEventCounter, 2);
|
||||
assert.equal(visibleEditorChangeEventCounter, 2);
|
||||
assert.ok(input.gotDisposed);
|
||||
|
||||
// Open again 2 inputs
|
||||
return service.openEditor(input, { pinned: true }).then(editor => {
|
||||
return service.openEditor(otherInput, { pinned: true }).then(editor => {
|
||||
assert.equal(service.visibleControls.length, 1);
|
||||
assert.equal(service.isOpen(input), true);
|
||||
assert.equal(service.isOpen(otherInput), true);
|
||||
|
||||
assert.equal(activeEditorChangeEventCounter, 4);
|
||||
assert.equal(visibleEditorChangeEventCounter, 4);
|
||||
|
||||
activeEditorChangeListener.dispose();
|
||||
visibleEditorChangeListener.dispose();
|
||||
didCloseEditorListener.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('openEditors() / replaceEditors()', function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart, 'id', false);
|
||||
part.create(document.createElement('div'));
|
||||
part.layout(400, 300);
|
||||
|
||||
const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part]));
|
||||
|
||||
const service: IEditorService = testInstantiationService.createInstance(EditorService);
|
||||
|
||||
const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-openEditors'));
|
||||
const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openEditors'));
|
||||
const replaceInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource3-openEditors'));
|
||||
|
||||
return part.whenRestored.then(() => {
|
||||
|
||||
// Open editors
|
||||
return service.openEditors([{ editor: input }, { editor: otherInput }]).then(() => {
|
||||
assert.equal(part.activeGroup.count, 2);
|
||||
|
||||
return service.replaceEditors([{ editor: input, replacement: replaceInput }], part.activeGroup).then(() => {
|
||||
assert.equal(part.activeGroup.count, 2);
|
||||
assert.equal(part.activeGroup.getIndexOfEditor(replaceInput), 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('caching', function () {
|
||||
const instantiationService = workbenchInstantiationService();
|
||||
const service: EditorService = <any>instantiationService.createInstance(EditorService);
|
||||
|
||||
// Cached Input (Files)
|
||||
const fileResource1 = toFileResource(this, '/foo/bar/cache1.js');
|
||||
const fileInput1 = service.createInput({ resource: fileResource1 });
|
||||
assert.ok(fileInput1);
|
||||
|
||||
const fileResource2 = toFileResource(this, '/foo/bar/cache2.js');
|
||||
const fileInput2 = service.createInput({ resource: fileResource2 });
|
||||
assert.ok(fileInput2);
|
||||
|
||||
assert.notEqual(fileInput1, fileInput2);
|
||||
|
||||
const fileInput1Again = service.createInput({ resource: fileResource1 });
|
||||
assert.equal(fileInput1Again, fileInput1);
|
||||
|
||||
fileInput1Again.dispose();
|
||||
|
||||
assert.ok(fileInput1.isDisposed());
|
||||
|
||||
const fileInput1AgainAndAgain = service.createInput({ resource: fileResource1 });
|
||||
assert.notEqual(fileInput1AgainAndAgain, fileInput1);
|
||||
assert.ok(!fileInput1AgainAndAgain.isDisposed());
|
||||
|
||||
// Cached Input (Resource)
|
||||
const resource1 = toResource.call(this, '/foo/bar/cache1.js');
|
||||
const input1 = service.createInput({ resource: resource1 });
|
||||
assert.ok(input1);
|
||||
|
||||
const resource2 = toResource.call(this, '/foo/bar/cache2.js');
|
||||
const input2 = service.createInput({ resource: resource2 });
|
||||
assert.ok(input2);
|
||||
|
||||
assert.notEqual(input1, input2);
|
||||
|
||||
const input1Again = service.createInput({ resource: resource1 });
|
||||
assert.equal(input1Again, input1);
|
||||
|
||||
input1Again.dispose();
|
||||
|
||||
assert.ok(input1.isDisposed());
|
||||
|
||||
const input1AgainAndAgain = service.createInput({ resource: resource1 });
|
||||
assert.notEqual(input1AgainAndAgain, input1);
|
||||
assert.ok(!input1AgainAndAgain.isDisposed());
|
||||
});
|
||||
|
||||
test('createInput', function () {
|
||||
const instantiationService = workbenchInstantiationService();
|
||||
const service: EditorService = <any>instantiationService.createInstance(EditorService);
|
||||
|
||||
// Untyped Input (file)
|
||||
let input = service.createInput({ resource: toFileResource(this, '/index.html'), options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof FileEditorInput);
|
||||
let contentInput = <FileEditorInput>input;
|
||||
assert.strictEqual(contentInput.getResource().fsPath, toFileResource(this, '/index.html').fsPath);
|
||||
|
||||
// Untyped Input (file, encoding)
|
||||
input = service.createInput({ resource: toFileResource(this, '/index.html'), encoding: 'utf16le', options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof FileEditorInput);
|
||||
contentInput = <FileEditorInput>input;
|
||||
assert.equal(contentInput.getPreferredEncoding(), 'utf16le');
|
||||
|
||||
// Untyped Input (untitled)
|
||||
input = service.createInput({ options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof UntitledEditorInput);
|
||||
|
||||
// Untyped Input (untitled with contents)
|
||||
input = service.createInput({ contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof UntitledEditorInput);
|
||||
|
||||
// Untyped Input (untitled with file path)
|
||||
input = service.createInput({ filePath: '/some/path.txt', options: { selection: { startLineNumber: 1, startColumn: 1 } } });
|
||||
assert(input instanceof UntitledEditorInput);
|
||||
assert.ok((input as UntitledEditorInput).hasAssociatedFilePath);
|
||||
});
|
||||
|
||||
test('delegate', function (done) {
|
||||
const instantiationService = workbenchInstantiationService();
|
||||
|
||||
class MyEditor extends BaseEditor {
|
||||
|
||||
constructor(id: string) {
|
||||
super(id, null, new TestThemeService(), new TestStorageService());
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'myEditor';
|
||||
}
|
||||
|
||||
layout(): void { }
|
||||
|
||||
createEditor(): any { }
|
||||
}
|
||||
|
||||
const ed = instantiationService.createInstance(MyEditor, 'my.editor');
|
||||
|
||||
const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate'));
|
||||
const delegate = instantiationService.createInstance(DelegatingEditorService);
|
||||
delegate.setEditorOpenHandler((group: IEditorGroup, input: IEditorInput, options?: EditorOptions) => {
|
||||
assert.strictEqual(input, inp);
|
||||
|
||||
done();
|
||||
|
||||
return Promise.resolve(ed);
|
||||
});
|
||||
|
||||
delegate.openEditor(inp);
|
||||
});
|
||||
|
||||
test('close editor does not dispose when editor opened in other group', function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart, 'id', false);
|
||||
part.create(document.createElement('div'));
|
||||
part.layout(400, 300);
|
||||
|
||||
const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part]));
|
||||
|
||||
const service: IEditorService = testInstantiationService.createInstance(EditorService);
|
||||
|
||||
const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-close1'));
|
||||
|
||||
const rootGroup = part.activeGroup;
|
||||
const rightGroup = part.addGroup(rootGroup, GroupDirection.RIGHT);
|
||||
|
||||
return part.whenRestored.then(() => {
|
||||
|
||||
// Open input
|
||||
return service.openEditor(input, { pinned: true }).then(editor => {
|
||||
return service.openEditor(input, { pinned: true }, rightGroup).then(editor => {
|
||||
const editors = service.editors;
|
||||
assert.equal(editors.length, 2);
|
||||
assert.equal(editors[0], input);
|
||||
assert.equal(editors[1], input);
|
||||
|
||||
// Close input
|
||||
return rootGroup.closeEditor(input).then(() => {
|
||||
assert.equal(input.isDisposed(), false);
|
||||
|
||||
return rightGroup.closeEditor(input).then(() => {
|
||||
assert.equal(input.isDisposed(), true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('open to the side', function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart, 'id', false);
|
||||
part.create(document.createElement('div'));
|
||||
part.layout(400, 300);
|
||||
|
||||
const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part]));
|
||||
|
||||
const service: IEditorService = testInstantiationService.createInstance(EditorService);
|
||||
|
||||
const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside'));
|
||||
const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside'));
|
||||
|
||||
const rootGroup = part.activeGroup;
|
||||
|
||||
return part.whenRestored.then(() => {
|
||||
return service.openEditor(input1, { pinned: true }, rootGroup).then(editor => {
|
||||
return service.openEditor(input1, { pinned: true, preserveFocus: true }, SIDE_GROUP).then(editor => {
|
||||
assert.equal(part.activeGroup, rootGroup);
|
||||
assert.equal(part.count, 2);
|
||||
assert.equal(editor.group, part.groups[1]);
|
||||
|
||||
// Open to the side uses existing neighbour group if any
|
||||
return service.openEditor(input2, { pinned: true, preserveFocus: true }, SIDE_GROUP).then(editor => {
|
||||
assert.equal(part.activeGroup, rootGroup);
|
||||
assert.equal(part.count, 2);
|
||||
assert.equal(editor.group, part.groups[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('active editor change / visible editor change events', async function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart, 'id', false);
|
||||
part.create(document.createElement('div'));
|
||||
part.layout(400, 300);
|
||||
|
||||
const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part]));
|
||||
|
||||
const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService);
|
||||
|
||||
const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active'));
|
||||
const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active'));
|
||||
|
||||
let activeEditorChangeEventFired = false;
|
||||
const activeEditorChangeListener = service.onDidActiveEditorChange(() => {
|
||||
activeEditorChangeEventFired = true;
|
||||
});
|
||||
|
||||
let visibleEditorChangeEventFired = false;
|
||||
const visibleEditorChangeListener = service.onDidVisibleEditorsChange(() => {
|
||||
visibleEditorChangeEventFired = true;
|
||||
});
|
||||
|
||||
function assertActiveEditorChangedEvent(expected: boolean) {
|
||||
assert.equal(activeEditorChangeEventFired, expected, `Unexpected active editor change state (got ${activeEditorChangeEventFired}, expected ${expected})`);
|
||||
activeEditorChangeEventFired = false;
|
||||
}
|
||||
|
||||
function assertVisibleEditorsChangedEvent(expected: boolean) {
|
||||
assert.equal(visibleEditorChangeEventFired, expected, `Unexpected visible editors change state (got ${visibleEditorChangeEventFired}, expected ${expected})`);
|
||||
visibleEditorChangeEventFired = false;
|
||||
}
|
||||
|
||||
async function closeEditorAndWaitForNextToOpen(group: IEditorGroup, input: EditorInput): Promise<void> {
|
||||
await group.closeEditor(input);
|
||||
await timeout(0); // closing an editor will not immediately open the next one, so we need to wait
|
||||
}
|
||||
|
||||
await part.whenRestored;
|
||||
|
||||
// 1.) open, open same, open other, close
|
||||
let editor = await service.openEditor(input, { pinned: true });
|
||||
const group = editor.group;
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
editor = await service.openEditor(input);
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
editor = await service.openEditor(otherInput);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
await closeEditorAndWaitForNextToOpen(group, otherInput);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
await closeEditorAndWaitForNextToOpen(group, input);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
// 2.) open, open same (forced open)
|
||||
editor = await service.openEditor(input);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
editor = await service.openEditor(input, { forceReload: true });
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await closeEditorAndWaitForNextToOpen(group, input);
|
||||
|
||||
// 3.) open, open inactive, close
|
||||
editor = await service.openEditor(input, { pinned: true });
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
editor = await service.openEditor(otherInput, { inactive: true });
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await group.closeAllEditors();
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
// 4.) open, open inactive, close inactive
|
||||
editor = await service.openEditor(input, { pinned: true });
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
editor = await service.openEditor(otherInput, { inactive: true });
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await closeEditorAndWaitForNextToOpen(group, otherInput);
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await group.closeAllEditors();
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
// 5.) add group, remove group
|
||||
editor = await service.openEditor(input, { pinned: true });
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
let rightGroup = part.addGroup(part.activeGroup, GroupDirection.RIGHT);
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
rightGroup.focus();
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
part.removeGroup(rightGroup);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await group.closeAllEditors();
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
// 6.) open editor in inactive group
|
||||
editor = await service.openEditor(input, { pinned: true });
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
rightGroup = part.addGroup(part.activeGroup, GroupDirection.RIGHT);
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await rightGroup.openEditor(otherInput);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
await closeEditorAndWaitForNextToOpen(rightGroup, otherInput);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
await group.closeAllEditors();
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
// 7.) activate group
|
||||
editor = await service.openEditor(input, { pinned: true });
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
rightGroup = part.addGroup(part.activeGroup, GroupDirection.RIGHT);
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await rightGroup.openEditor(otherInput);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
group.focus();
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await closeEditorAndWaitForNextToOpen(rightGroup, otherInput);
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
await group.closeAllEditors();
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
// 8.) move editor
|
||||
editor = await service.openEditor(input, { pinned: true });
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
editor = await service.openEditor(otherInput, { pinned: true });
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
group.moveEditor(otherInput, group, { index: 0 });
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await group.closeAllEditors();
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
// 9.) close editor in inactive group
|
||||
editor = await service.openEditor(input, { pinned: true });
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
rightGroup = part.addGroup(part.activeGroup, GroupDirection.RIGHT);
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(false);
|
||||
|
||||
await rightGroup.openEditor(otherInput);
|
||||
assertActiveEditorChangedEvent(true);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
await closeEditorAndWaitForNextToOpen(group, input);
|
||||
assertActiveEditorChangedEvent(false);
|
||||
assertVisibleEditorsChangedEvent(true);
|
||||
|
||||
// cleanup
|
||||
activeEditorChangeListener.dispose();
|
||||
visibleEditorChangeListener.dispose();
|
||||
});
|
||||
|
||||
test('openEditor returns NULL when opening fails or is inactive', async function () {
|
||||
const partInstantiator = workbenchInstantiationService();
|
||||
|
||||
const part = partInstantiator.createInstance(EditorPart, 'id', false);
|
||||
part.create(document.createElement('div'));
|
||||
part.layout(400, 300);
|
||||
|
||||
const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part]));
|
||||
|
||||
const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService);
|
||||
|
||||
const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active'));
|
||||
const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-inactive'));
|
||||
const failingInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource3-failing'));
|
||||
failingInput.setFailToOpen();
|
||||
|
||||
await part.whenRestored;
|
||||
|
||||
let editor = await service.openEditor(input, { pinned: true });
|
||||
assert.ok(editor);
|
||||
|
||||
let otherEditor = await service.openEditor(otherInput, { inactive: true });
|
||||
assert.ok(!otherEditor);
|
||||
|
||||
let failingEditor = await service.openEditor(failingInput);
|
||||
assert.ok(!failingEditor);
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
/*
|
||||
function toResource(path: string) {
|
||||
return URI.from({ scheme: 'custom', path });
|
||||
}
|
||||
|
||||
function toFileResource(self: any, path: string) {
|
||||
return URI.file(paths.join('C:\\', Buffer.from(self.test.fullTitle()).toString('base64'), path));
|
||||
}
|
||||
*/
|
||||
@@ -8,35 +8,20 @@ import Severity from 'vs/base/common/severity';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IExtensionPoint } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionIdentifier, IExtensionManifest, IExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
|
||||
export interface IExtensionDescription {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
export interface IExtensionDescription extends IExtensionManifest {
|
||||
readonly identifier: ExtensionIdentifier;
|
||||
readonly uuid?: string;
|
||||
readonly displayName?: string;
|
||||
readonly version: string;
|
||||
readonly publisher: string;
|
||||
readonly isBuiltin: boolean;
|
||||
readonly isUnderDevelopment: boolean;
|
||||
readonly extensionLocation: URI;
|
||||
readonly extensionDependencies?: string[];
|
||||
readonly activationEvents?: string[];
|
||||
readonly engines: {
|
||||
vscode: string;
|
||||
// {{SQL CARBON EDIT}}
|
||||
azdata?: string;
|
||||
};
|
||||
readonly main?: string;
|
||||
readonly contributes?: { [point: string]: any; };
|
||||
readonly keywords?: string[];
|
||||
readonly repository?: {
|
||||
url: string;
|
||||
};
|
||||
enableProposedApi?: boolean;
|
||||
}
|
||||
|
||||
export const nullExtensionDescription = Object.freeze(<IExtensionDescription>{
|
||||
id: 'nullExtensionDescription',
|
||||
identifier: new ExtensionIdentifier('nullExtensionDescription'),
|
||||
name: 'Null Extension Description',
|
||||
version: '0.0.0',
|
||||
publisher: 'vscode',
|
||||
@@ -51,7 +36,7 @@ export const IExtensionService = createDecorator<IExtensionService>('extensionSe
|
||||
export interface IMessage {
|
||||
type: Severity;
|
||||
message: string;
|
||||
extensionId: string;
|
||||
extensionId: ExtensionIdentifier;
|
||||
extensionPointId: string;
|
||||
}
|
||||
|
||||
@@ -104,7 +89,7 @@ export interface IExtensionHostProfile {
|
||||
/**
|
||||
* Extension id or one of the four known program states.
|
||||
*/
|
||||
export type ProfileSegmentId = string | 'idle' | 'program' | 'gc' | 'self';
|
||||
export type ProfileSegmentId = string | 'idle' | 'program' | 'gc' | 'self' | null;
|
||||
|
||||
export class ActivationTimes {
|
||||
constructor(
|
||||
@@ -131,7 +116,7 @@ export const ExtensionHostLogFileName = 'exthost';
|
||||
|
||||
export interface IWillActivateEvent {
|
||||
readonly event: string;
|
||||
readonly activation: Thenable<void>;
|
||||
readonly activation: Promise<void>;
|
||||
}
|
||||
|
||||
export interface IResponsiveStateChangeEvent {
|
||||
@@ -156,7 +141,12 @@ export interface IExtensionService extends ICpuProfilerTarget {
|
||||
* Fired when extensions status changes.
|
||||
* The event contains the ids of the extensions that have changed.
|
||||
*/
|
||||
onDidChangeExtensionsStatus: Event<string[]>;
|
||||
onDidChangeExtensionsStatus: Event<ExtensionIdentifier[]>;
|
||||
|
||||
/**
|
||||
* Fired when the available extensions change (i.e. when extensions are added or removed).
|
||||
*/
|
||||
onDidChangeExtensions: Event<void>;
|
||||
|
||||
/**
|
||||
* An event that is fired when activation happens.
|
||||
@@ -172,7 +162,7 @@ export interface IExtensionService extends ICpuProfilerTarget {
|
||||
/**
|
||||
* Send an activation event and activate interested extensions.
|
||||
*/
|
||||
activateByEvent(activationEvent: string): Thenable<void>;
|
||||
activateByEvent(activationEvent: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* An promise that resolves when the installed extensions are registered after
|
||||
@@ -191,6 +181,18 @@ export interface IExtensionService extends ICpuProfilerTarget {
|
||||
*/
|
||||
getExtension(id: string): Promise<IExtensionDescription | undefined>;
|
||||
|
||||
/**
|
||||
* Returns `true` if the given extension can be added. Otherwise `false`.
|
||||
* @param extension An extension
|
||||
*/
|
||||
canAddExtension(extension: IExtensionDescription): boolean;
|
||||
|
||||
/**
|
||||
* Returns `true` if the given extension can be removed. Otherwise `false`.
|
||||
* @param extension An extension
|
||||
*/
|
||||
canRemoveExtension(extension: IExtensionDescription): boolean;
|
||||
|
||||
/**
|
||||
* Read all contributions to an extension point.
|
||||
*/
|
||||
@@ -246,5 +248,14 @@ export function checkProposedApiEnabled(extension: IExtensionDescription): void
|
||||
}
|
||||
|
||||
export function throwProposedApiError(extension: IExtensionDescription): never {
|
||||
throw new Error(`[${extension.id}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.id}`);
|
||||
throw new Error(`[${extension.identifier.value}]: Proposed API is only available when running out of dev or with the following command line switch: --enable-proposed-api ${extension.identifier.value}`);
|
||||
}
|
||||
|
||||
export function toExtension(extensionDescription: IExtensionDescription): IExtension {
|
||||
return {
|
||||
type: extensionDescription.isBuiltin ? ExtensionType.System : ExtensionType.User,
|
||||
identifier: { id: getGalleryExtensionId(extensionDescription.publisher, extensionDescription.name), uuid: extensionDescription.uuid },
|
||||
manifest: extensionDescription,
|
||||
location: extensionDescription.extensionLocation,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/co
|
||||
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IExtensionDescription, IMessage } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
|
||||
@@ -35,7 +36,7 @@ export class ExtensionMessageCollector {
|
||||
this._messageHandler({
|
||||
type: type,
|
||||
message: message,
|
||||
extensionId: this._extension.id,
|
||||
extensionId: this._extension.identifier,
|
||||
extensionPointId: this._extensionPointId
|
||||
});
|
||||
}
|
||||
@@ -60,7 +61,7 @@ export interface IExtensionPointUser<T> {
|
||||
}
|
||||
|
||||
export interface IExtensionPointHandler<T> {
|
||||
(extensions: IExtensionPointUser<T>[]): void;
|
||||
(extensions: IExtensionPointUser<T>[], delta: ExtensionPointUserDelta<T>): void;
|
||||
}
|
||||
|
||||
export interface IExtensionPoint<T> {
|
||||
@@ -68,22 +69,60 @@ export interface IExtensionPoint<T> {
|
||||
setHandler(handler: IExtensionPointHandler<T>): void;
|
||||
}
|
||||
|
||||
export class ExtensionPointUserDelta<T> {
|
||||
|
||||
private static _toSet<T>(arr: IExtensionPointUser<T>[]): Set<string> {
|
||||
const result = new Set<string>();
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
result.add(ExtensionIdentifier.toKey(arr[i].description.identifier));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static compute<T>(previous: IExtensionPointUser<T>[] | null, current: IExtensionPointUser<T>[]): ExtensionPointUserDelta<T> {
|
||||
if (!previous || !previous.length) {
|
||||
return new ExtensionPointUserDelta<T>(current, []);
|
||||
}
|
||||
if (!current || !current.length) {
|
||||
return new ExtensionPointUserDelta<T>([], previous);
|
||||
}
|
||||
|
||||
const previousSet = this._toSet(previous);
|
||||
const currentSet = this._toSet(current);
|
||||
|
||||
let added = current.filter(user => !previousSet.has(ExtensionIdentifier.toKey(user.description.identifier)));
|
||||
let removed = previous.filter(user => !currentSet.has(ExtensionIdentifier.toKey(user.description.identifier)));
|
||||
|
||||
return new ExtensionPointUserDelta<T>(added, removed);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly added: IExtensionPointUser<T>[],
|
||||
public readonly removed: IExtensionPointUser<T>[],
|
||||
) { }
|
||||
}
|
||||
|
||||
export class ExtensionPoint<T> implements IExtensionPoint<T> {
|
||||
|
||||
public readonly name: string;
|
||||
private _handler: IExtensionPointHandler<T> | null;
|
||||
private _users: IExtensionPointUser<T>[] | null;
|
||||
private _done: boolean;
|
||||
public readonly isDynamic: boolean;
|
||||
|
||||
constructor(name: string) {
|
||||
private _handler: IExtensionPointHandler<T> | null;
|
||||
private _handlerCalled: boolean;
|
||||
private _users: IExtensionPointUser<T>[] | null;
|
||||
private _delta: ExtensionPointUserDelta<T> | null;
|
||||
|
||||
constructor(name: string, isDynamic: boolean) {
|
||||
this.name = name;
|
||||
this.isDynamic = isDynamic;
|
||||
this._handler = null;
|
||||
this._handlerCalled = false;
|
||||
this._users = null;
|
||||
this._done = false;
|
||||
this._delta = null;
|
||||
}
|
||||
|
||||
setHandler(handler: IExtensionPointHandler<T>): void {
|
||||
if (this._handler !== null || this._done) {
|
||||
if (this._handler !== null) {
|
||||
throw new Error('Handler already set!');
|
||||
}
|
||||
this._handler = handler;
|
||||
@@ -91,27 +130,23 @@ export class ExtensionPoint<T> implements IExtensionPoint<T> {
|
||||
}
|
||||
|
||||
acceptUsers(users: IExtensionPointUser<T>[]): void {
|
||||
if (this._users !== null || this._done) {
|
||||
throw new Error('Users already set!');
|
||||
}
|
||||
this._delta = ExtensionPointUserDelta.compute(this._users, users);
|
||||
this._users = users;
|
||||
this._handle();
|
||||
}
|
||||
|
||||
private _handle(): void {
|
||||
if (this._handler === null || this._users === null) {
|
||||
if (this._handler === null || this._users === null || this._delta === null) {
|
||||
return;
|
||||
}
|
||||
this._done = true;
|
||||
|
||||
let handler = this._handler;
|
||||
this._handler = null;
|
||||
|
||||
let users = this._users;
|
||||
this._users = null;
|
||||
if (this._handlerCalled && !this.isDynamic) {
|
||||
throw new Error('The extension point is not dynamic!');
|
||||
}
|
||||
|
||||
try {
|
||||
handler(users);
|
||||
this._handlerCalled = true;
|
||||
this._handler(this._users, this._delta);
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
@@ -331,6 +366,13 @@ export const schema = {
|
||||
}
|
||||
};
|
||||
|
||||
export interface IExtensionPointDescriptor {
|
||||
isDynamic?: boolean;
|
||||
extensionPoint: string;
|
||||
deps?: IExtensionPoint<any>[];
|
||||
jsonSchema: IJSONSchema;
|
||||
}
|
||||
|
||||
export class ExtensionsRegistryImpl {
|
||||
|
||||
private _extensionPoints: { [extPoint: string]: ExtensionPoint<any>; };
|
||||
@@ -339,14 +381,14 @@ export class ExtensionsRegistryImpl {
|
||||
this._extensionPoints = {};
|
||||
}
|
||||
|
||||
public registerExtensionPoint<T>(extensionPoint: string, deps: IExtensionPoint<any>[], jsonSchema: IJSONSchema): IExtensionPoint<T> {
|
||||
if (hasOwnProperty.call(this._extensionPoints, extensionPoint)) {
|
||||
throw new Error('Duplicate extension point: ' + extensionPoint);
|
||||
public registerExtensionPoint<T>(desc: IExtensionPointDescriptor): IExtensionPoint<T> {
|
||||
if (hasOwnProperty.call(this._extensionPoints, desc.extensionPoint)) {
|
||||
throw new Error('Duplicate extension point: ' + desc.extensionPoint);
|
||||
}
|
||||
let result = new ExtensionPoint<T>(extensionPoint);
|
||||
this._extensionPoints[extensionPoint] = result;
|
||||
let result = new ExtensionPoint<T>(desc.extensionPoint, desc.isDynamic || false);
|
||||
this._extensionPoints[desc.extensionPoint] = result;
|
||||
|
||||
schema.properties['contributes'].properties[extensionPoint] = jsonSchema;
|
||||
schema.properties['contributes'].properties[desc.extensionPoint] = desc.jsonSchema;
|
||||
schemaRegistry.registerSchema(schemaId, schema);
|
||||
|
||||
return result;
|
||||
@@ -355,6 +397,10 @@ export class ExtensionsRegistryImpl {
|
||||
public getExtensionPoints(): ExtensionPoint<any>[] {
|
||||
return Object.keys(this._extensionPoints).map(point => this._extensionPoints[point]);
|
||||
}
|
||||
|
||||
public getExtensionPointsMap(): { [extPoint: string]: ExtensionPoint<any>; } {
|
||||
return this._extensionPoints;
|
||||
}
|
||||
}
|
||||
|
||||
const PRExtensions = {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { URI } from 'vs/base/common/uri';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE } from 'vs/platform/extensions/common/extensions';
|
||||
import { BUILTIN_MANIFEST_CACHE_FILE, MANIFEST_CACHE_FOLDER, USER_MANIFEST_CACHE_FILE, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import pkg from 'vs/platform/node/package';
|
||||
import product from 'vs/platform/node/product';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
@@ -50,6 +50,7 @@ export class CachedExtensionScanner {
|
||||
public readonly scannedExtensions: Promise<IExtensionDescription[]>;
|
||||
private _scannedExtensionsResolve: (result: IExtensionDescription[]) => void;
|
||||
private _scannedExtensionsReject: (err: any) => void;
|
||||
public readonly translationConfig: Promise<Translations>;
|
||||
|
||||
constructor(
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@@ -61,31 +62,58 @@ export class CachedExtensionScanner {
|
||||
this._scannedExtensionsResolve = resolve;
|
||||
this._scannedExtensionsReject = reject;
|
||||
});
|
||||
this.translationConfig = CachedExtensionScanner._readTranslationConfig();
|
||||
}
|
||||
|
||||
public startScanningExtensions(log: ILog): void {
|
||||
CachedExtensionScanner._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, this._extensionEnablementService, log)
|
||||
.then(({ system, user, development }) => {
|
||||
let result: { [extensionId: string]: IExtensionDescription; } = {};
|
||||
system.forEach((systemExtension) => {
|
||||
result[systemExtension.id] = systemExtension;
|
||||
});
|
||||
user.forEach((userExtension) => {
|
||||
if (result.hasOwnProperty(userExtension.id)) {
|
||||
log.warn(userExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[userExtension.id].extensionLocation.fsPath, userExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result[userExtension.id] = userExtension;
|
||||
});
|
||||
development.forEach(developedExtension => {
|
||||
log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath));
|
||||
if (result.hasOwnProperty(developedExtension.id)) {
|
||||
log.warn(developedExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", result[developedExtension.id].extensionLocation.fsPath, developedExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result[developedExtension.id] = developedExtension;
|
||||
});
|
||||
return Object.keys(result).map(name => result[name]);
|
||||
})
|
||||
.then(this._scannedExtensionsResolve, this._scannedExtensionsReject);
|
||||
public async scanSingleExtension(path: string, isBuiltin: boolean, log: ILog): Promise<IExtensionDescription | null> {
|
||||
const translations = await this.translationConfig;
|
||||
|
||||
const version = pkg.version;
|
||||
const commit = product.commit;
|
||||
const devMode = !!process.env['VSCODE_DEV'];
|
||||
const locale = platform.locale;
|
||||
const input = new ExtensionScannerInput(version, commit, locale, devMode, path, isBuiltin, false, translations);
|
||||
return ExtensionScanner.scanSingleExtension(input, log);
|
||||
}
|
||||
|
||||
public async startScanningExtensions(log: ILog): Promise<void> {
|
||||
try {
|
||||
const translations = await this.translationConfig;
|
||||
const { system, user, development } = await CachedExtensionScanner._scanInstalledExtensions(this._windowService, this._notificationService, this._environmentService, this._extensionEnablementService, log, translations);
|
||||
|
||||
let result = new Map<string, IExtensionDescription>();
|
||||
system.forEach((systemExtension) => {
|
||||
const extensionKey = ExtensionIdentifier.toKey(systemExtension.identifier);
|
||||
const extension = result.get(extensionKey);
|
||||
if (extension) {
|
||||
log.warn(systemExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, systemExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result.set(extensionKey, systemExtension);
|
||||
});
|
||||
user.forEach((userExtension) => {
|
||||
const extensionKey = ExtensionIdentifier.toKey(userExtension.identifier);
|
||||
const extension = result.get(extensionKey);
|
||||
if (extension) {
|
||||
log.warn(userExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, userExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result.set(extensionKey, userExtension);
|
||||
});
|
||||
development.forEach(developedExtension => {
|
||||
log.info('', nls.localize('extensionUnderDevelopment', "Loading development extension at {0}", developedExtension.extensionLocation.fsPath));
|
||||
const extensionKey = ExtensionIdentifier.toKey(developedExtension.identifier);
|
||||
const extension = result.get(extensionKey);
|
||||
if (extension) {
|
||||
log.warn(developedExtension.extensionLocation.fsPath, nls.localize('overwritingExtension', "Overwriting extension {0} with {1}.", extension.extensionLocation.fsPath, developedExtension.extensionLocation.fsPath));
|
||||
}
|
||||
result.set(extensionKey, developedExtension);
|
||||
});
|
||||
let r: IExtensionDescription[] = [];
|
||||
result.forEach((value) => r.push(value));
|
||||
|
||||
this._scannedExtensionsResolve(r);
|
||||
} catch (err) {
|
||||
this._scannedExtensionsReject(err);
|
||||
}
|
||||
}
|
||||
|
||||
private static async _validateExtensionsCache(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, cacheKey: string, input: ExtensionScannerInput): Promise<void> {
|
||||
@@ -198,86 +226,90 @@ export class CachedExtensionScanner {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _scanInstalledExtensions(windowService: IWindowService, notificationService: INotificationService, environmentService: IEnvironmentService, extensionEnablementService: IExtensionEnablementService, log: ILog): Promise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
|
||||
|
||||
const translationConfig: Promise<Translations> = platform.translationsConfigFile
|
||||
? pfs.readFile(platform.translationsConfigFile, 'utf8').then((content) => {
|
||||
try {
|
||||
return JSON.parse(content) as Translations;
|
||||
} catch (err) {
|
||||
return Object.create(null);
|
||||
}
|
||||
}, (err) => {
|
||||
return Object.create(null);
|
||||
})
|
||||
: Promise.resolve(Object.create(null));
|
||||
|
||||
return translationConfig.then((translations) => {
|
||||
const version = pkg.version;
|
||||
const commit = product.commit;
|
||||
const devMode = !!process.env['VSCODE_DEV'];
|
||||
const locale = platform.locale;
|
||||
|
||||
const builtinExtensions = this._scanExtensionsWithCache(
|
||||
windowService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
BUILTIN_MANIFEST_CACHE_FILE,
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations),
|
||||
log
|
||||
);
|
||||
|
||||
let finalBuiltinExtensions: Promise<IExtensionDescription[]> = builtinExtensions;
|
||||
|
||||
if (devMode) {
|
||||
const builtInExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', 'builtInExtensions.json'));
|
||||
const builtInExtensions = pfs.readFile(builtInExtensionsFilePath, 'utf8')
|
||||
.then<IBuiltInExtension[]>(raw => JSON.parse(raw));
|
||||
|
||||
const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json');
|
||||
const controlFile = pfs.readFile(controlFilePath, 'utf8')
|
||||
.then<IBuiltInExtensionControl>(raw => JSON.parse(raw), () => ({} as any));
|
||||
|
||||
const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations);
|
||||
const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile])
|
||||
.then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control))
|
||||
.then(resolver => ExtensionScanner.scanExtensions(input, log, resolver));
|
||||
|
||||
finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions);
|
||||
private static async _readTranslationConfig(): Promise<Translations> {
|
||||
if (platform.translationsConfigFile) {
|
||||
try {
|
||||
const content = await pfs.readFile(platform.translationsConfigFile, 'utf8');
|
||||
return JSON.parse(content) as Translations;
|
||||
} catch (err) {
|
||||
// no problemo
|
||||
}
|
||||
}
|
||||
return Object.create(null);
|
||||
}
|
||||
|
||||
const userExtensions = (
|
||||
extensionEnablementService.allUserExtensionsDisabled || !environmentService.extensionsPath
|
||||
? Promise.resolve([])
|
||||
: this._scanExtensionsWithCache(
|
||||
windowService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
USER_MANIFEST_CACHE_FILE,
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations),
|
||||
log
|
||||
)
|
||||
private static _scanInstalledExtensions(
|
||||
windowService: IWindowService,
|
||||
notificationService: INotificationService,
|
||||
environmentService: IEnvironmentService,
|
||||
extensionEnablementService: IExtensionEnablementService,
|
||||
log: ILog,
|
||||
translations: Translations
|
||||
): Promise<{ system: IExtensionDescription[], user: IExtensionDescription[], development: IExtensionDescription[] }> {
|
||||
|
||||
const version = pkg.version;
|
||||
const commit = product.commit;
|
||||
const devMode = !!process.env['VSCODE_DEV'];
|
||||
const locale = platform.locale;
|
||||
|
||||
const builtinExtensions = this._scanExtensionsWithCache(
|
||||
windowService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
BUILTIN_MANIFEST_CACHE_FILE,
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, getSystemExtensionsRoot(), true, false, translations),
|
||||
log
|
||||
);
|
||||
|
||||
let finalBuiltinExtensions: Promise<IExtensionDescription[]> = builtinExtensions;
|
||||
|
||||
if (devMode) {
|
||||
const builtInExtensionsFilePath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'build', 'builtInExtensions.json'));
|
||||
const builtInExtensions = pfs.readFile(builtInExtensionsFilePath, 'utf8')
|
||||
.then<IBuiltInExtension[]>(raw => JSON.parse(raw));
|
||||
|
||||
const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json');
|
||||
const controlFile = pfs.readFile(controlFilePath, 'utf8')
|
||||
.then<IBuiltInExtensionControl>(raw => JSON.parse(raw), () => ({} as any));
|
||||
|
||||
const input = new ExtensionScannerInput(version, commit, locale, devMode, getExtraDevSystemExtensionsRoot(), true, false, translations);
|
||||
const extraBuiltinExtensions = Promise.all([builtInExtensions, controlFile])
|
||||
.then(([builtInExtensions, control]) => new ExtraBuiltInExtensionResolver(builtInExtensions, control))
|
||||
.then(resolver => ExtensionScanner.scanExtensions(input, log, resolver));
|
||||
|
||||
finalBuiltinExtensions = ExtensionScanner.mergeBuiltinExtensions(builtinExtensions, extraBuiltinExtensions);
|
||||
}
|
||||
|
||||
const userExtensions = (
|
||||
extensionEnablementService.allUserExtensionsDisabled || !environmentService.extensionsPath
|
||||
? Promise.resolve([])
|
||||
: this._scanExtensionsWithCache(
|
||||
windowService,
|
||||
notificationService,
|
||||
environmentService,
|
||||
USER_MANIFEST_CACHE_FILE,
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, environmentService.extensionsPath, false, false, translations),
|
||||
log
|
||||
)
|
||||
);
|
||||
|
||||
// Always load developed extensions while extensions development
|
||||
let developedExtensions: Promise<IExtensionDescription[]> = Promise.resolve([]);
|
||||
if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI && environmentService.extensionDevelopmentLocationURI.scheme === Schemas.file) {
|
||||
developedExtensions = ExtensionScanner.scanOneOrMultipleExtensions(
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, fsPath(environmentService.extensionDevelopmentLocationURI), false, true, translations), log
|
||||
);
|
||||
}
|
||||
|
||||
// Always load developed extensions while extensions development
|
||||
let developedExtensions: Promise<IExtensionDescription[]> = Promise.resolve([]);
|
||||
if (environmentService.isExtensionDevelopment && environmentService.extensionDevelopmentLocationURI && environmentService.extensionDevelopmentLocationURI.scheme === Schemas.file) {
|
||||
developedExtensions = ExtensionScanner.scanOneOrMultipleExtensions(
|
||||
new ExtensionScannerInput(version, commit, locale, devMode, fsPath(environmentService.extensionDevelopmentLocationURI), false, true, translations), log
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => {
|
||||
const system = extensionDescriptions[0];
|
||||
const user = extensionDescriptions[1];
|
||||
const development = extensionDescriptions[2];
|
||||
return { system, user, development };
|
||||
}).then(null, err => {
|
||||
log.error('', err);
|
||||
return { system: [], user: [], development: [] };
|
||||
});
|
||||
return Promise.all([finalBuiltinExtensions, userExtensions, developedExtensions]).then((extensionDescriptions: IExtensionDescription[][]) => {
|
||||
const system = extensionDescriptions[0];
|
||||
const user = extensionDescriptions[1];
|
||||
const development = extensionDescriptions[2];
|
||||
return { system, user, development };
|
||||
}).then(undefined, err => {
|
||||
log.error('', err);
|
||||
return { system: [], user: [], development: [] };
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ import { Server, Socket, createServer } from 'net';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Emitter, Event, anyEvent, debounceEvent, fromNodeEventEmitter, mapEvent } from 'vs/base/common/event';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
@@ -21,9 +20,8 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { IRemoteConsoleLog, log, parse } from 'vs/base/node/console';
|
||||
import { findFreePort, randomPort } from 'vs/base/node/ports';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Protocol, generateRandomPipeName } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { Protocol, generateRandomPipeName, BufferedProtocol } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { IBroadcast, IBroadcastService } from 'vs/platform/broadcast/electron-browser/broadcastService';
|
||||
import { getScopes } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { EXTENSION_ATTACH_BROADCAST_CHANNEL, EXTENSION_CLOSE_EXTHOST_BROADCAST_CHANNEL, EXTENSION_LOG_BROADCAST_CHANNEL, EXTENSION_RELOAD_BROADCAST_CHANNEL, EXTENSION_TERMINATE_BROADCAST_CHANNEL } from 'vs/platform/extensions/common/extensionHost';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
@@ -34,15 +32,14 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfigurationInitData, IInitData } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { IInitData } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/common/extensionHostProtocol';
|
||||
import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration';
|
||||
import { ICrashReporterService } from 'vs/workbench/services/crashReporter/electron-browser/crashReporterService';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
export interface IExtensionHostStarter {
|
||||
readonly onCrashed: Event<[number, string]>;
|
||||
start(): Thenable<IMessagePassingProtocol>;
|
||||
start(): Promise<IMessagePassingProtocol>;
|
||||
getInspectPort(): number;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -103,7 +100,6 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
@IBroadcastService private readonly _broadcastService: IBroadcastService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@IWorkspaceConfigurationService private readonly _configurationService: IWorkspaceConfigurationService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@ICrashReporterService private readonly _crashReporterService: ICrashReporterService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@@ -176,7 +172,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
|
||||
const opts = {
|
||||
env: objects.mixin(objects.deepClone(process.env), {
|
||||
AMD_ENTRYPOINT: 'vs/workbench/node/extensionHostProcess',
|
||||
AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: true,
|
||||
VSCODE_IPC_HOOK_EXTHOST: pipeName,
|
||||
@@ -217,15 +213,15 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
type Output = { data: string, format: string[] };
|
||||
this._extensionHostProcess.stdout.setEncoding('utf8');
|
||||
this._extensionHostProcess.stderr.setEncoding('utf8');
|
||||
const onStdout = fromNodeEventEmitter<string>(this._extensionHostProcess.stdout, 'data');
|
||||
const onStderr = fromNodeEventEmitter<string>(this._extensionHostProcess.stderr, 'data');
|
||||
const onOutput = anyEvent(
|
||||
mapEvent(onStdout, o => ({ data: `%c${o}`, format: [''] })),
|
||||
mapEvent(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
|
||||
const onStdout = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stdout, 'data');
|
||||
const onStderr = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stderr, 'data');
|
||||
const onOutput = Event.any(
|
||||
Event.map(onStdout, o => ({ data: `%c${o}`, format: [''] })),
|
||||
Event.map(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
|
||||
);
|
||||
|
||||
// Debounce all output, so we can render it in the Chrome console as a group
|
||||
const onDebouncedOutput = debounceEvent<Output>(onOutput, (r, o) => {
|
||||
const onDebouncedOutput = Event.debounce<Output>(onOutput, (r, o) => {
|
||||
return r
|
||||
? { data: r.data + o.data, format: [...r.format, ...o.format] }
|
||||
: { data: o.data, format: o.format };
|
||||
@@ -356,7 +352,11 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
this._namedPipeServer.close();
|
||||
this._namedPipeServer = null;
|
||||
this._extensionHostConnection = socket;
|
||||
resolve(new Protocol(this._extensionHostConnection));
|
||||
|
||||
// using a buffered message protocol here because between now
|
||||
// and the first time a `then` executes some messages might be lost
|
||||
// unless we immediately register a listener for `onMessage`.
|
||||
resolve(new BufferedProtocol(new Protocol(this._extensionHostConnection)));
|
||||
});
|
||||
|
||||
}).then((protocol) => {
|
||||
@@ -402,10 +402,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
disposable.dispose();
|
||||
|
||||
// release this promise
|
||||
// using a buffered message protocol here because between now
|
||||
// and the first time a `then` executes some messages might be lost
|
||||
// unless we immediately register a listener for `onMessage`.
|
||||
resolve(new BufferedMessagePassingProtocol(protocol));
|
||||
resolve(protocol);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -420,15 +417,14 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
private _createExtHostInitData(): Promise<IInitData> {
|
||||
return Promise.all([this._telemetryService.getTelemetryInfo(), this._extensions])
|
||||
.then(([telemetryInfo, extensionDescriptions]) => {
|
||||
const configurationData: IConfigurationInitData = { ...this._configurationService.getConfigurationData(), configurationScopes: {} };
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
const r: IInitData = {
|
||||
commit: product.commit,
|
||||
parentPid: process.pid,
|
||||
environment: {
|
||||
isExtensionDevelopmentDebug: this._isExtensionDevDebug,
|
||||
appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : void 0,
|
||||
appSettingsHome: this._environmentService.appSettingsHome ? URI.file(this._environmentService.appSettingsHome) : void 0,
|
||||
appRoot: this._environmentService.appRoot ? URI.file(this._environmentService.appRoot) : undefined,
|
||||
appSettingsHome: this._environmentService.appSettingsHome ? URI.file(this._environmentService.appSettingsHome) : undefined,
|
||||
extensionDevelopmentLocationURI: this._environmentService.extensionDevelopmentLocationURI,
|
||||
extensionTestsPath: this._environmentService.extensionTestsPath,
|
||||
globalStorageHome: URI.file(this._environmentService.globalStorageHome)
|
||||
@@ -439,9 +435,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace)
|
||||
},
|
||||
resolvedExtensions: [],
|
||||
extensions: extensionDescriptions,
|
||||
// Send configurations scopes only in development mode.
|
||||
configuration: !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment ? { ...configurationData, configurationScopes: getScopes() } : configurationData,
|
||||
telemetryInfo,
|
||||
logLevel: this._logService.getLevel(),
|
||||
logsLocation: this._extensionHostLogsLocation,
|
||||
@@ -574,57 +569,3 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will ensure no messages are lost from creation time until the first user of onMessage comes in.
|
||||
*/
|
||||
class BufferedMessagePassingProtocol implements IMessagePassingProtocol {
|
||||
|
||||
private readonly _actual: IMessagePassingProtocol;
|
||||
private _bufferedMessagesListener: IDisposable;
|
||||
private _bufferedMessages: Buffer[];
|
||||
|
||||
constructor(actual: IMessagePassingProtocol) {
|
||||
this._actual = actual;
|
||||
this._bufferedMessages = [];
|
||||
this._bufferedMessagesListener = this._actual.onMessage((buff) => this._bufferedMessages.push(buff));
|
||||
}
|
||||
|
||||
public send(buffer: Buffer): void {
|
||||
this._actual.send(buffer);
|
||||
}
|
||||
|
||||
public onMessage(listener: (e: Buffer) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable {
|
||||
if (!this._bufferedMessages) {
|
||||
// second caller gets nothing
|
||||
return this._actual.onMessage(listener, thisArgs, disposables);
|
||||
}
|
||||
|
||||
// prepare result
|
||||
const result = this._actual.onMessage(listener, thisArgs, disposables);
|
||||
|
||||
// stop listening to buffered messages
|
||||
this._bufferedMessagesListener.dispose();
|
||||
|
||||
// capture buffered messages
|
||||
const bufferedMessages = this._bufferedMessages;
|
||||
this._bufferedMessages = null;
|
||||
|
||||
// it is important to deliver these messages after this call, but before
|
||||
// other messages have a chance to be received (to guarantee in order delivery)
|
||||
// that's why we're using here nextTick and not other types of timeouts
|
||||
process.nextTick(() => {
|
||||
// deliver buffered messages
|
||||
while (bufferedMessages.length > 0) {
|
||||
const msg = bufferedMessages.shift();
|
||||
try {
|
||||
listener.call(thisArgs, msg);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,27 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ExtHostCustomersRegistry } from 'vs/workbench/api/electron-browser/extHostCustomers';
|
||||
import { ExtHostContext, ExtHostExtensionServiceShape, IExtHostContext, MainContext } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { ProfileSession } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ProfileSession, IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IExtensionHostStarter } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { ExtensionHostProfiler } from 'vs/workbench/services/extensions/electron-browser/extensionHostProfiler';
|
||||
import { ProxyIdentifier } from 'vs/workbench/services/extensions/node/proxyIdentifier';
|
||||
import { IRPCProtocolLogger, RPCProtocol, RequestInitiator, ResponsiveState } from 'vs/workbench/services/extensions/node/rpcProtocol';
|
||||
import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IUntitledResourceInput } from 'vs/workbench/common/editor';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
|
||||
// Enable to see detailed message communication between window and extension host
|
||||
const LOG_EXTENSION_HOST_COMMUNICATION = false;
|
||||
const LOG_USE_COLORS = true;
|
||||
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(void 0);
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
export class ExtensionHostProcessManager extends Disposable {
|
||||
|
||||
@@ -42,7 +51,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
/**
|
||||
* winjs believes a proxy is a promise because it has a `then` method, so wrap the result in an object.
|
||||
*/
|
||||
private _extensionHostProcessProxy: Thenable<{ value: ExtHostExtensionServiceShape; }>;
|
||||
private _extensionHostProcessProxy: Promise<{ value: ExtHostExtensionServiceShape; }>;
|
||||
|
||||
constructor(
|
||||
extensionHostProcessWorker: IExtensionHostStarter,
|
||||
@@ -70,6 +79,9 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
);
|
||||
this._extensionHostProcessProxy.then(() => {
|
||||
initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent));
|
||||
this._register(registerLatencyTestProvider({
|
||||
measure: () => this.measure()
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,12 +105,63 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
// {{SQL CARBON EDIT}} - Add new getExtensionHostProcessWorker method
|
||||
public getExtenstionHostProcessWorker(): IExtensionHostStarter {
|
||||
return this._extensionHostProcessWorker;
|
||||
}
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
|
||||
private async measure(): Promise<ExtHostLatencyResult> {
|
||||
const latency = await this._measureLatency();
|
||||
const down = await this._measureDown();
|
||||
const up = await this._measureUp();
|
||||
return {
|
||||
remoteAuthority: this._remoteAuthority,
|
||||
latency,
|
||||
down,
|
||||
up
|
||||
};
|
||||
}
|
||||
|
||||
private async _measureLatency(): Promise<number> {
|
||||
const COUNT = 10;
|
||||
|
||||
const { value: proxy } = await this._extensionHostProcessProxy;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
const sw = StopWatch.create(true);
|
||||
await proxy.$test_latency(i);
|
||||
sw.stop();
|
||||
sum += sw.elapsed();
|
||||
}
|
||||
return (sum / COUNT);
|
||||
}
|
||||
|
||||
private static _convert(byteCount: number, elapsedMillis: number): number {
|
||||
return (byteCount * 1000 * 8) / elapsedMillis;
|
||||
}
|
||||
|
||||
private async _measureUp(): Promise<number> {
|
||||
const SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
const { value: proxy } = await this._extensionHostProcessProxy;
|
||||
let b = Buffer.alloc(SIZE, Math.random() % 256);
|
||||
const sw = StopWatch.create(true);
|
||||
await proxy.$test_up(b);
|
||||
sw.stop();
|
||||
return ExtensionHostProcessManager._convert(SIZE, sw.elapsed());
|
||||
}
|
||||
|
||||
private async _measureDown(): Promise<number> {
|
||||
const SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
const { value: proxy } = await this._extensionHostProcessProxy;
|
||||
const sw = StopWatch.create(true);
|
||||
await proxy.$test_down(SIZE);
|
||||
sw.stop();
|
||||
return ExtensionHostProcessManager._convert(SIZE, sw.elapsed());
|
||||
}
|
||||
|
||||
public canProfileExtensionHost(): boolean {
|
||||
return this._extensionHostProcessWorker && Boolean(this._extensionHostProcessWorker.getInspectPort());
|
||||
}
|
||||
@@ -130,8 +193,7 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
|
||||
// Customers
|
||||
const customers = ExtHostCustomersRegistry.getCustomers();
|
||||
for (let i = 0, len = customers.length; i < len; i++) {
|
||||
const ctor = customers[i];
|
||||
for (const ctor of customers) {
|
||||
const instance = this._instantiationService.createInstance(ctor, extHostContext);
|
||||
this._extensionHostProcessCustomers.push(instance);
|
||||
}
|
||||
@@ -143,7 +205,13 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
return this._extensionHostProcessRPCProtocol.getProxy(ExtHostContext.ExtHostExtensionService);
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Thenable<void> {
|
||||
public activate(extension: ExtensionIdentifier, activationEvent: string): Promise<void> {
|
||||
return this._extensionHostProcessProxy.then((proxy) => {
|
||||
return proxy.value.$activate(extension, activationEvent);
|
||||
});
|
||||
}
|
||||
|
||||
public activateByEvent(activationEvent: string): Promise<void> {
|
||||
if (this._extensionHostProcessFinishedActivateEvents[activationEvent] || !this._extensionHostProcessProxy) {
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
@@ -179,13 +247,28 @@ export class ExtensionHostProcessManager extends Disposable {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public resolveAuthority(remoteAuthority: string): Thenable<ResolvedAuthority> {
|
||||
public resolveAuthority(remoteAuthority: string): Promise<ResolvedAuthority> {
|
||||
const authorityPlusIndex = remoteAuthority.indexOf('+');
|
||||
if (authorityPlusIndex === -1) {
|
||||
// This authority does not need to be resolved, simply parse the port number
|
||||
const pieces = remoteAuthority.split(':');
|
||||
return Promise.resolve({
|
||||
authority: remoteAuthority,
|
||||
host: pieces[0],
|
||||
port: parseInt(pieces[1], 10),
|
||||
syncExtensions: false
|
||||
});
|
||||
}
|
||||
return this._extensionHostProcessProxy.then(proxy => proxy.value.$resolveAuthority(remoteAuthority));
|
||||
}
|
||||
|
||||
public start(enabledExtensionIds: string[]): Thenable<void> {
|
||||
public start(enabledExtensionIds: ExtensionIdentifier[]): Promise<void> {
|
||||
return this._extensionHostProcessProxy.then(proxy => proxy.value.$startExtensionHost(enabledExtensionIds));
|
||||
}
|
||||
|
||||
public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]): Promise<void> {
|
||||
return this._extensionHostProcessProxy.then(proxy => proxy.value.$deltaExtensions(toAdd, toRemove));
|
||||
}
|
||||
}
|
||||
|
||||
const colorTables = [
|
||||
@@ -230,7 +313,7 @@ class RPCLogger implements IRPCProtocolLogger {
|
||||
} else {
|
||||
args.push(data);
|
||||
}
|
||||
console.log.apply(console, args);
|
||||
console.log.apply(console, args as [string, ...string[]]);
|
||||
}
|
||||
|
||||
logIncoming(msgLength: number, req: number, initiator: RequestInitiator, str: string, data?: any): void {
|
||||
@@ -243,3 +326,68 @@ class RPCLogger implements IRPCProtocolLogger {
|
||||
this._log('Win \u2192 Ext', this._totalOutgoing, msgLength, req, initiator, str, data);
|
||||
}
|
||||
}
|
||||
|
||||
interface ExtHostLatencyResult {
|
||||
remoteAuthority: string;
|
||||
up: number;
|
||||
down: number;
|
||||
latency: number;
|
||||
}
|
||||
|
||||
interface ExtHostLatencyProvider {
|
||||
measure(): Promise<ExtHostLatencyResult>;
|
||||
}
|
||||
|
||||
let providers: ExtHostLatencyProvider[] = [];
|
||||
function registerLatencyTestProvider(provider: ExtHostLatencyProvider): IDisposable {
|
||||
providers.push(provider);
|
||||
return {
|
||||
dispose: () => {
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
if (providers[i] === provider) {
|
||||
providers.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getLatencyTestProviders(): ExtHostLatencyProvider[] {
|
||||
return providers.slice(0);
|
||||
}
|
||||
|
||||
export class MeasureExtHostLatencyAction extends Action {
|
||||
public static readonly ID = 'editor.action.measureExtHostLatency';
|
||||
public static readonly LABEL = nls.localize('measureExtHostLatency', "Measure Extension Host Latency");
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
label: string,
|
||||
@IEditorService private readonly _editorService: IEditorService
|
||||
) {
|
||||
super(id, label);
|
||||
}
|
||||
|
||||
public async run(): Promise<any> {
|
||||
const measurements = await Promise.all(getLatencyTestProviders().map(provider => provider.measure()));
|
||||
this._editorService.openEditor({ contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } } as IUntitledResourceInput);
|
||||
}
|
||||
|
||||
private static _print(m: ExtHostLatencyResult): string {
|
||||
return `${m.remoteAuthority ? `Authority: ${m.remoteAuthority}\n` : ``}Roundtrip latency: ${m.latency.toFixed(3)}ms\nUp: ${MeasureExtHostLatencyAction._printSpeed(m.up)}\nDown: ${MeasureExtHostLatencyAction._printSpeed(m.down)}\n`;
|
||||
}
|
||||
|
||||
private static _printSpeed(n: number): string {
|
||||
if (n <= 1024) {
|
||||
return `${n} bps`;
|
||||
}
|
||||
if (n < 1024 * 1024) {
|
||||
return `${(n / 1024).toFixed(1)} kbps`;
|
||||
}
|
||||
return `${(n / 1024 / 1024).toFixed(1)} Mbps`;
|
||||
}
|
||||
}
|
||||
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(new SyncActionDescriptor(MeasureExtHostLatencyAction, MeasureExtHostLatencyAction.ID, MeasureExtHostLatencyAction.LABEL), 'Developer: Measure Extension Host Latency', nls.localize('developer', "Developer"));
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ExtensionHostProfiler {
|
||||
|
||||
public async start(): Promise<ProfileSession> {
|
||||
const profiler = await import('v8-inspect-profiler');
|
||||
const session = await profiler.startProfiling({ port: this._port });
|
||||
const session = await profiler.startProfiling({ port: this._port, checkForPaused: true });
|
||||
return {
|
||||
stop: async () => {
|
||||
const profile = await session.stop();
|
||||
@@ -56,26 +56,29 @@ export class ExtensionHostProfiler {
|
||||
} else if (segmentId === 'self' && node.callFrame.url) {
|
||||
let extension = searchTree.findSubstr(node.callFrame.url);
|
||||
if (extension) {
|
||||
segmentId = extension.id;
|
||||
segmentId = extension.identifier.value;
|
||||
}
|
||||
}
|
||||
idsToSegmentId.set(node.id, segmentId);
|
||||
|
||||
if (node.children) {
|
||||
for (let child of node.children) {
|
||||
visit(idsToNodes.get(child), segmentId);
|
||||
for (const child of node.children) {
|
||||
const childNode = idsToNodes.get(child);
|
||||
if (childNode) {
|
||||
visit(childNode, segmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(nodes[0], null);
|
||||
|
||||
let samples = profile.samples;
|
||||
let timeDeltas = profile.timeDeltas;
|
||||
const samples = profile.samples || [];
|
||||
let timeDeltas = profile.timeDeltas || [];
|
||||
let distilledDeltas: number[] = [];
|
||||
let distilledIds: ProfileSegmentId[] = [];
|
||||
|
||||
let currSegmentTime = 0;
|
||||
let currSegmentId: string = void 0;
|
||||
let currSegmentId: string | undefined;
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
let id = samples[i];
|
||||
let segmentId = idsToSegmentId.get(id);
|
||||
@@ -84,7 +87,7 @@ export class ExtensionHostProfiler {
|
||||
distilledIds.push(currSegmentId);
|
||||
distilledDeltas.push(currSegmentTime);
|
||||
}
|
||||
currSegmentId = segmentId;
|
||||
currSegmentId = segmentId || undefined;
|
||||
currSegmentTime = 0;
|
||||
}
|
||||
currSegmentTime += timeDeltas[i];
|
||||
@@ -93,9 +96,6 @@ export class ExtensionHostProfiler {
|
||||
distilledIds.push(currSegmentId);
|
||||
distilledDeltas.push(currSegmentTime);
|
||||
}
|
||||
idsToNodes = null;
|
||||
idsToSegmentId = null;
|
||||
searchTree = null;
|
||||
|
||||
return {
|
||||
startTime: profile.startTime,
|
||||
|
||||
@@ -13,8 +13,8 @@ import * as perf from 'vs/base/common/performance';
|
||||
import { isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionIdentifier, IExtensionManagementService, LocalExtensionType } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BetterMergeId, areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { BetterMergeId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import pkg from 'vs/platform/node/package';
|
||||
@@ -22,19 +22,40 @@ import product from 'vs/platform/node/product';
|
||||
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { ActivationTimes, ExtensionPointContribution, IExtensionDescription, IExtensionService, IExtensionsStatus, IMessage, ProfileSession, IWillActivateEvent, IResponsiveStateChangeEvent } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ActivationTimes, ExtensionPointContribution, IExtensionDescription, IExtensionService, IExtensionsStatus, IMessage, ProfileSession, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser, schema } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
|
||||
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/node/extensionDescriptionRegistry';
|
||||
import { ResponsiveState } from 'vs/workbench/services/extensions/node/rpcProtocol';
|
||||
import { CachedExtensionScanner, Logger } from 'vs/workbench/services/extensions/electron-browser/cachedExtensionScanner';
|
||||
import { ExtensionHostProcessManager } from 'vs/workbench/services/extensions/electron-browser/extensionHostProcessManager';
|
||||
import { ExtensionIdentifier, IExtension, ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(void 0);
|
||||
const NO_OP_VOID_PROMISE = Promise.resolve<void>(undefined);
|
||||
|
||||
schema.properties.engines.properties.vscode.default = `^${pkg.version}`;
|
||||
|
||||
let productAllowProposedApi: Set<string> = null;
|
||||
function allowProposedApiFromProduct(id: ExtensionIdentifier): boolean {
|
||||
// create set if needed
|
||||
if (productAllowProposedApi === null) {
|
||||
productAllowProposedApi = new Set<string>();
|
||||
if (isNonEmptyArray(product.extensionAllowedProposedApi)) {
|
||||
product.extensionAllowedProposedApi.forEach((id) => productAllowProposedApi.add(ExtensionIdentifier.toKey(id)));
|
||||
}
|
||||
}
|
||||
return productAllowProposedApi.has(ExtensionIdentifier.toKey(id));
|
||||
}
|
||||
|
||||
class DeltaExtensionsQueueItem {
|
||||
constructor(
|
||||
public readonly toAdd: IExtension[],
|
||||
public readonly toRemove: string[]
|
||||
) { }
|
||||
}
|
||||
|
||||
export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
public _serviceBrand: any;
|
||||
@@ -43,15 +64,19 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
private _registry: ExtensionDescriptionRegistry;
|
||||
private readonly _installedExtensionsReady: Barrier;
|
||||
private readonly _isDev: boolean;
|
||||
private readonly _extensionsMessages: { [id: string]: IMessage[] };
|
||||
private readonly _extensionsMessages: Map<string, IMessage[]>;
|
||||
private _allRequestedActivateEvents: { [activationEvent: string]: boolean; };
|
||||
private readonly _extensionScanner: CachedExtensionScanner;
|
||||
private _deltaExtensionsQueue: DeltaExtensionsQueueItem[];
|
||||
|
||||
private readonly _onDidRegisterExtensions: Emitter<void> = this._register(new Emitter<void>({ leakWarningThreshold: 500 }));
|
||||
private readonly _onDidRegisterExtensions: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidRegisterExtensions = this._onDidRegisterExtensions.event;
|
||||
|
||||
private readonly _onDidChangeExtensionsStatus: Emitter<string[]> = this._register(new Emitter<string[]>());
|
||||
public readonly onDidChangeExtensionsStatus: Event<string[]> = this._onDidChangeExtensionsStatus.event;
|
||||
private readonly _onDidChangeExtensionsStatus: Emitter<ExtensionIdentifier[]> = this._register(new Emitter<ExtensionIdentifier[]>());
|
||||
public readonly onDidChangeExtensionsStatus: Event<ExtensionIdentifier[]> = this._onDidChangeExtensionsStatus.event;
|
||||
|
||||
private readonly _onDidChangeExtensions: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onDidChangeExtensions: Event<void> = this._onDidChangeExtensions.event;
|
||||
|
||||
private readonly _onWillActivateByEvent = this._register(new Emitter<IWillActivateEvent>());
|
||||
public readonly onWillActivateByEvent: Event<IWillActivateEvent> = this._onWillActivateByEvent.event;
|
||||
@@ -61,8 +86,9 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
// --- Members used per extension host process
|
||||
private _extensionHostProcessManagers: ExtensionHostProcessManager[];
|
||||
private _extensionHostProcessActivationTimes: { [id: string]: ActivationTimes; };
|
||||
private _extensionHostExtensionRuntimeErrors: { [id: string]: Error[]; };
|
||||
private _extensionHostActiveExtensions: Map<string, ExtensionIdentifier>;
|
||||
private _extensionHostProcessActivationTimes: Map<string, ActivationTimes>;
|
||||
private _extensionHostExtensionRuntimeErrors: Map<string, Error[]>;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@@ -70,22 +96,24 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IExtensionEnablementService private readonly _extensionEnablementService: IExtensionEnablementService,
|
||||
@IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService,
|
||||
@IWindowService private readonly _windowService: IWindowService,
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
|
||||
@IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService
|
||||
@ILifecycleService private readonly _lifecycleService: ILifecycleService
|
||||
) {
|
||||
super();
|
||||
this._extensionHostLogsLocation = URI.file(path.posix.join(this._environmentService.logsPath, `exthost${this._windowService.getCurrentWindowId()}`));
|
||||
this._registry = null;
|
||||
this._installedExtensionsReady = new Barrier();
|
||||
this._isDev = !this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment;
|
||||
this._extensionsMessages = {};
|
||||
this._extensionsMessages = new Map<string, IMessage[]>();
|
||||
this._allRequestedActivateEvents = Object.create(null);
|
||||
this._extensionScanner = this._instantiationService.createInstance(CachedExtensionScanner);
|
||||
this._deltaExtensionsQueue = [];
|
||||
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostProcessActivationTimes = Object.create(null);
|
||||
this._extensionHostExtensionRuntimeErrors = Object.create(null);
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
this._startDelayed(this._lifecycleService);
|
||||
|
||||
@@ -97,6 +125,255 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
this._extensionEnablementService.onEnablementChanged((extensions) => {
|
||||
let toAdd: IExtension[] = [];
|
||||
let toRemove: string[] = [];
|
||||
for (const extension of extensions) {
|
||||
if (this._extensionEnablementService.isEnabled(extension)) {
|
||||
// an extension has been enabled
|
||||
toAdd.push(extension);
|
||||
} else {
|
||||
// an extension has been disabled
|
||||
toRemove.push(extension.identifier.id);
|
||||
}
|
||||
}
|
||||
this._handleDeltaExtensions(new DeltaExtensionsQueueItem(toAdd, toRemove));
|
||||
});
|
||||
|
||||
this._extensionManagementService.onDidInstallExtension((event) => {
|
||||
if (event.local) {
|
||||
if (this._extensionEnablementService.isEnabled(event.local)) {
|
||||
// an extension has been installed
|
||||
this._handleDeltaExtensions(new DeltaExtensionsQueueItem([event.local], []));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._extensionManagementService.onDidUninstallExtension((event) => {
|
||||
if (!event.error) {
|
||||
// an extension has been uninstalled
|
||||
this._handleDeltaExtensions(new DeltaExtensionsQueueItem([], [event.identifier.id]));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _inHandleDeltaExtensions = false;
|
||||
private async _handleDeltaExtensions(item: DeltaExtensionsQueueItem): Promise<void> {
|
||||
this._deltaExtensionsQueue.push(item);
|
||||
if (this._inHandleDeltaExtensions) {
|
||||
// Let the current item finish, the new one will be picked up
|
||||
return;
|
||||
}
|
||||
|
||||
while (this._deltaExtensionsQueue.length > 0) {
|
||||
const item = this._deltaExtensionsQueue.shift();
|
||||
try {
|
||||
this._inHandleDeltaExtensions = true;
|
||||
await this._deltaExtensions(item.toAdd, item.toRemove);
|
||||
} finally {
|
||||
this._inHandleDeltaExtensions = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _deltaExtensions(_toAdd: IExtension[], _toRemove: string[]): Promise<void> {
|
||||
if (this._windowService.getConfiguration().remoteAuthority) {
|
||||
return;
|
||||
}
|
||||
|
||||
let toAdd: IExtensionDescription[] = [];
|
||||
for (let i = 0, len = _toAdd.length; i < len; i++) {
|
||||
const extension = _toAdd[i];
|
||||
|
||||
if (extension.location.scheme !== Schemas.file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingExtensionDescription = this._registry.getExtensionDescription(extension.identifier.id);
|
||||
if (existingExtensionDescription) {
|
||||
// this extension is already running (most likely at a different version)
|
||||
continue;
|
||||
}
|
||||
|
||||
const extensionDescription = await this._extensionScanner.scanSingleExtension(extension.location.fsPath, extension.type === ExtensionType.System, this.createLogger());
|
||||
if (!extensionDescription || !this._usesOnlyDynamicExtensionPoints(extensionDescription)) {
|
||||
// uses non-dynamic extension point
|
||||
continue;
|
||||
}
|
||||
|
||||
toAdd.push(extensionDescription);
|
||||
}
|
||||
|
||||
let toRemove: IExtensionDescription[] = [];
|
||||
for (let i = 0, len = _toRemove.length; i < len; i++) {
|
||||
const extensionId = _toRemove[i];
|
||||
const extensionDescription = this._registry.getExtensionDescription(extensionId);
|
||||
if (!extensionDescription) {
|
||||
// ignore disabling/uninstalling an extension which is not running
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this._canRemoveExtension(extensionDescription)) {
|
||||
// uses non-dynamic extension point or is activated
|
||||
continue;
|
||||
}
|
||||
|
||||
toRemove.push(extensionDescription);
|
||||
}
|
||||
|
||||
if (toAdd.length === 0 && toRemove.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the local registry
|
||||
this._registry.deltaExtensions(toAdd, toRemove.map(e => e.identifier));
|
||||
|
||||
// Update extension points
|
||||
this._rehandleExtensionPoints((<IExtensionDescription[]>[]).concat(toAdd).concat(toRemove));
|
||||
|
||||
// Update the extension host
|
||||
if (this._extensionHostProcessManagers.length > 0) {
|
||||
await this._extensionHostProcessManagers[0].deltaExtensions(toAdd, toRemove.map(e => e.identifier));
|
||||
}
|
||||
|
||||
this._onDidChangeExtensions.fire(undefined);
|
||||
|
||||
for (let i = 0; i < toAdd.length; i++) {
|
||||
this._activateAddedExtensionIfNeeded(toAdd[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private _rehandleExtensionPoints(extensionDescriptions: IExtensionDescription[]): void {
|
||||
const affectedExtensionPoints: { [extPointName: string]: boolean; } = Object.create(null);
|
||||
for (let extensionDescription of extensionDescriptions) {
|
||||
if (extensionDescription.contributes) {
|
||||
for (let extPointName in extensionDescription.contributes) {
|
||||
if (hasOwnProperty.call(extensionDescription.contributes, extPointName)) {
|
||||
affectedExtensionPoints[extPointName] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const messageHandler = (msg: IMessage) => this._handleExtensionPointMessage(msg);
|
||||
|
||||
const availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
const extensionPoints = ExtensionsRegistry.getExtensionPoints();
|
||||
for (let i = 0, len = extensionPoints.length; i < len; i++) {
|
||||
if (affectedExtensionPoints[extensionPoints[i].name]) {
|
||||
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _usesOnlyDynamicExtensionPoints(extension: IExtensionDescription): boolean {
|
||||
const extensionPoints = ExtensionsRegistry.getExtensionPointsMap();
|
||||
if (extension.contributes) {
|
||||
for (let extPointName in extension.contributes) {
|
||||
if (hasOwnProperty.call(extension.contributes, extPointName)) {
|
||||
const extPoint = extensionPoints[extPointName];
|
||||
if (extPoint) {
|
||||
if (!extPoint.isDynamic) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// This extension has a 3rd party (unknown) extension point
|
||||
// ===> require a reload for now...
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public canAddExtension(extension: IExtensionDescription): boolean {
|
||||
if (this._windowService.getConfiguration().remoteAuthority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extension.extensionLocation.scheme !== Schemas.file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionDescription = this._registry.getExtensionDescription(extension.identifier);
|
||||
if (extensionDescription) {
|
||||
// ignore adding an extension which is already running and cannot be removed
|
||||
if (!this._canRemoveExtension(extensionDescription)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return this._usesOnlyDynamicExtensionPoints(extension);
|
||||
}
|
||||
|
||||
public canRemoveExtension(extension: IExtensionDescription): boolean {
|
||||
if (this._windowService.getConfiguration().remoteAuthority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extension.extensionLocation.scheme !== Schemas.file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionDescription = this._registry.getExtensionDescription(extension.identifier);
|
||||
if (!extensionDescription) {
|
||||
// ignore removing an extension which is not running
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._canRemoveExtension(extensionDescription);
|
||||
}
|
||||
|
||||
private _canRemoveExtension(extension: IExtensionDescription): boolean {
|
||||
if (this._extensionHostActiveExtensions.has(ExtensionIdentifier.toKey(extension.identifier))) {
|
||||
// Extension is running, cannot remove it safely
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._usesOnlyDynamicExtensionPoints(extension);
|
||||
}
|
||||
|
||||
private async _activateAddedExtensionIfNeeded(extensionDescription: IExtensionDescription): Promise<void> {
|
||||
|
||||
let shouldActivate = false;
|
||||
let shouldActivateReason: string | null = null;
|
||||
if (Array.isArray(extensionDescription.activationEvents)) {
|
||||
for (let activationEvent of extensionDescription.activationEvents) {
|
||||
// TODO@joao: there's no easy way to contribute this
|
||||
if (activationEvent === 'onUri') {
|
||||
activationEvent = `onUri:${ExtensionIdentifier.toKey(extensionDescription.identifier)}`;
|
||||
}
|
||||
|
||||
if (this._allRequestedActivateEvents[activationEvent]) {
|
||||
// This activation event was fired before the extension was added
|
||||
shouldActivate = true;
|
||||
shouldActivateReason = activationEvent;
|
||||
break;
|
||||
}
|
||||
|
||||
if (activationEvent === '*') {
|
||||
shouldActivate = true;
|
||||
shouldActivateReason = activationEvent;
|
||||
break;
|
||||
}
|
||||
|
||||
if (/^workspaceContains/.test(activationEvent)) {
|
||||
// do not trigger a search, just activate in this case...
|
||||
shouldActivate = true;
|
||||
shouldActivateReason = activationEvent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldActivate) {
|
||||
await Promise.all(
|
||||
this._extensionHostProcessManagers.map(extHostManager => extHostManager.activate(extensionDescription.identifier, shouldActivateReason))
|
||||
).then(() => { });
|
||||
}
|
||||
}
|
||||
|
||||
private _startDelayed(lifecycleService: ILifecycleService): void {
|
||||
@@ -137,14 +414,18 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
|
||||
private _stopExtensionHostProcess(): void {
|
||||
const previouslyActivatedExtensionIds = Object.keys(this._extensionHostProcessActivationTimes);
|
||||
let previouslyActivatedExtensionIds: ExtensionIdentifier[] = [];
|
||||
this._extensionHostActiveExtensions.forEach((value) => {
|
||||
previouslyActivatedExtensionIds.push(value);
|
||||
});
|
||||
|
||||
for (let i = 0; i < this._extensionHostProcessManagers.length; i++) {
|
||||
this._extensionHostProcessManagers[i].dispose();
|
||||
for (const manager of this._extensionHostProcessManagers) {
|
||||
manager.dispose();
|
||||
}
|
||||
this._extensionHostProcessManagers = [];
|
||||
this._extensionHostProcessActivationTimes = Object.create(null);
|
||||
this._extensionHostExtensionRuntimeErrors = Object.create(null);
|
||||
this._extensionHostActiveExtensions = new Map<string, ExtensionIdentifier>();
|
||||
this._extensionHostProcessActivationTimes = new Map<string, ActivationTimes>();
|
||||
this._extensionHostExtensionRuntimeErrors = new Map<string, Error[]>();
|
||||
|
||||
if (previouslyActivatedExtensionIds.length > 0) {
|
||||
this._onDidChangeExtensionsStatus.fire(previouslyActivatedExtensionIds);
|
||||
@@ -154,7 +435,18 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
private _startExtensionHostProcess(isInitialStart: boolean, initialActivationEvents: string[]): void {
|
||||
this._stopExtensionHostProcess();
|
||||
|
||||
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, !isInitialStart, this.getExtensions(), this._extensionHostLogsLocation);
|
||||
let autoStart: boolean;
|
||||
let extensions: Promise<IExtensionDescription[]>;
|
||||
if (isInitialStart) {
|
||||
autoStart = false;
|
||||
extensions = this._extensionScanner.scannedExtensions;
|
||||
} else {
|
||||
// restart case
|
||||
autoStart = true;
|
||||
extensions = this.getExtensions();
|
||||
}
|
||||
|
||||
const extHostProcessWorker = this._instantiationService.createInstance(ExtensionHostProcessWorker, autoStart, extensions, this._extensionHostLogsLocation);
|
||||
const extHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, extHostProcessWorker, null, initialActivationEvents);
|
||||
extHostProcessManager.onDidCrash(([code, signal]) => this._onExtensionHostCrashed(code, signal));
|
||||
extHostProcessManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ target: extHostProcessManager, isResponsive: responsiveState === ResponsiveState.Responsive }); });
|
||||
@@ -205,14 +497,14 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
if (this._installedExtensionsReady.isOpen()) {
|
||||
// Extensions have been scanned and interpreted
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents[activationEvent] = true;
|
||||
|
||||
if (!this._registry.containsActivationEvent(activationEvent)) {
|
||||
// There is no extension that is interested in this activation event
|
||||
return NO_OP_VOID_PROMISE;
|
||||
}
|
||||
|
||||
// Record the fact that this activationEvent was requested (in case of a restart)
|
||||
this._allRequestedActivateEvents[activationEvent] = true;
|
||||
|
||||
return this._activateByEvent(activationEvent);
|
||||
} else {
|
||||
// Extensions have not been scanned yet.
|
||||
@@ -272,13 +564,12 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
let result: { [id: string]: IExtensionsStatus; } = Object.create(null);
|
||||
if (this._registry) {
|
||||
const extensions = this._registry.getAllExtensionDescriptions();
|
||||
for (let i = 0, len = extensions.length; i < len; i++) {
|
||||
const extension = extensions[i];
|
||||
const id = extension.id;
|
||||
result[id] = {
|
||||
messages: this._extensionsMessages[id],
|
||||
activationTimes: this._extensionHostProcessActivationTimes[id],
|
||||
runtimeErrors: this._extensionHostExtensionRuntimeErrors[id],
|
||||
for (const extension of extensions) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extension.identifier);
|
||||
result[extension.identifier.value] = {
|
||||
messages: this._extensionsMessages.get(extensionKey),
|
||||
activationTimes: this._extensionHostProcessActivationTimes.get(extensionKey),
|
||||
runtimeErrors: this._extensionHostExtensionRuntimeErrors.get(extensionKey),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -316,23 +607,29 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
|
||||
// --- impl
|
||||
|
||||
private async _scanAndHandleExtensions(): Promise<void> {
|
||||
this._extensionScanner.startScanningExtensions(new Logger((severity, source, message) => {
|
||||
private createLogger(): Logger {
|
||||
return new Logger((severity, source, message) => {
|
||||
if (this._isDev && source) {
|
||||
this._logOrShowMessage(severity, `[${source}]: ${message}`);
|
||||
} else {
|
||||
this._logOrShowMessage(severity, message);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private async _scanAndHandleExtensions(): Promise<void> {
|
||||
this._extensionScanner.startScanningExtensions(this.createLogger());
|
||||
|
||||
const extensionHost = this._extensionHostProcessManagers[0];
|
||||
const extensions = await this._extensionScanner.scannedExtensions;
|
||||
const enabledExtensions = await this._getRuntimeExtensions(extensions);
|
||||
extensionHost.start(enabledExtensions.map(extension => extension.id));
|
||||
this._onHasExtensions(enabledExtensions);
|
||||
|
||||
this._handleExtensionPoints(enabledExtensions);
|
||||
extensionHost.start(enabledExtensions.map(extension => extension.identifier));
|
||||
this._releaseBarrier();
|
||||
}
|
||||
|
||||
private _onHasExtensions(allExtensions: IExtensionDescription[]): void {
|
||||
private _handleExtensionPoints(allExtensions: IExtensionDescription[]): void {
|
||||
this._registry = new ExtensionDescriptionRegistry(allExtensions);
|
||||
|
||||
let availableExtensions = this._registry.getAllExtensionDescriptions();
|
||||
@@ -343,58 +640,65 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
for (let i = 0, len = extensionPoints.length; i < len; i++) {
|
||||
ExtensionService._handleExtensionPoint(extensionPoints[i], availableExtensions, messageHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private _releaseBarrier(): void {
|
||||
perf.mark('extensionHostReady');
|
||||
this._installedExtensionsReady.open();
|
||||
this._onDidRegisterExtensions.fire(void 0);
|
||||
this._onDidChangeExtensionsStatus.fire(availableExtensions.map(e => e.id));
|
||||
this._onDidRegisterExtensions.fire(undefined);
|
||||
this._onDidChangeExtensionsStatus.fire(this._registry.getAllExtensionDescriptions().map(e => e.identifier));
|
||||
}
|
||||
|
||||
private _getRuntimeExtensions(allExtensions: IExtensionDescription[]): Promise<IExtensionDescription[]> {
|
||||
return this._extensionEnablementService.getDisabledExtensions()
|
||||
.then(disabledExtensions => {
|
||||
|
||||
const result: { [extensionId: string]: IExtensionDescription; } = {};
|
||||
const extensionsToDisable: IExtensionIdentifier[] = [];
|
||||
const runtimeExtensions: IExtensionDescription[] = [];
|
||||
const extensionsToDisable: IExtensionDescription[] = [];
|
||||
const userMigratedSystemExtensions: IExtensionIdentifier[] = [{ id: BetterMergeId }];
|
||||
|
||||
const enableProposedApiFor: string | string[] = this._environmentService.args['enable-proposed-api'] || [];
|
||||
let enableProposedApiFor: string | string[] = this._environmentService.args['enable-proposed-api'] || [];
|
||||
|
||||
const notFound = (id: string) => nls.localize('notFound', "Extension \`{0}\` cannot use PROPOSED API as it cannot be found", id);
|
||||
|
||||
if (enableProposedApiFor.length) {
|
||||
let allProposed = (enableProposedApiFor instanceof Array ? enableProposedApiFor : [enableProposedApiFor]);
|
||||
allProposed.forEach(id => {
|
||||
if (!allExtensions.some(description => description.id === id)) {
|
||||
if (!allExtensions.some(description => ExtensionIdentifier.equals(description.identifier, id))) {
|
||||
console.error(notFound(id));
|
||||
}
|
||||
});
|
||||
// Make enabled proposed API be lowercase for case insensitive comparison
|
||||
if (Array.isArray(enableProposedApiFor)) {
|
||||
enableProposedApiFor = enableProposedApiFor.map(id => id.toLowerCase());
|
||||
} else {
|
||||
enableProposedApiFor = enableProposedApiFor.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
const enableProposedApiForAll = !this._environmentService.isBuilt ||
|
||||
(!!this._environmentService.extensionDevelopmentLocationURI && product.nameLong.indexOf('Insiders') >= 0) ||
|
||||
(!!this._environmentService.extensionDevelopmentLocationURI && product.nameLong !== 'Visual Studio Code') ||
|
||||
(enableProposedApiFor.length === 0 && 'enable-proposed-api' in this._environmentService.args);
|
||||
|
||||
for (const extension of allExtensions) {
|
||||
const isExtensionUnderDevelopment = this._environmentService.isExtensionDevelopment && isEqualOrParent(extension.extensionLocation, this._environmentService.extensionDevelopmentLocationURI);
|
||||
// Do not disable extensions under development
|
||||
if (!isExtensionUnderDevelopment) {
|
||||
if (disabledExtensions.some(disabled => areSameExtensions(disabled, extension))) {
|
||||
if (disabledExtensions.some(disabled => areSameExtensions(disabled, { id: extension.identifier.value }))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!extension.isBuiltin) {
|
||||
// Check if the extension is changed to system extension
|
||||
const userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: extension.id }))[0];
|
||||
const userMigratedSystemExtension = userMigratedSystemExtensions.filter(userMigratedSystemExtension => areSameExtensions(userMigratedSystemExtension, { id: extension.identifier.value }))[0];
|
||||
if (userMigratedSystemExtension) {
|
||||
extensionsToDisable.push(userMigratedSystemExtension);
|
||||
extensionsToDisable.push(extension);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result[extension.id] = this._updateEnableProposedApi(extension, enableProposedApiForAll, enableProposedApiFor);
|
||||
runtimeExtensions.push(this._updateEnableProposedApi(extension, enableProposedApiForAll, enableProposedApiFor));
|
||||
}
|
||||
const runtimeExtensions = Object.keys(result).map(name => result[name]);
|
||||
|
||||
this._telemetryService.publicLog('extensionsScanned', {
|
||||
totalCount: runtimeExtensions.length,
|
||||
@@ -402,14 +706,8 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
});
|
||||
|
||||
if (extensionsToDisable.length) {
|
||||
return this._extensionManagementService.getInstalled(LocalExtensionType.User)
|
||||
.then(installed => {
|
||||
const toDisable = installed.filter(i => extensionsToDisable.some(e => areSameExtensions({ id: getGalleryExtensionIdFromLocal(i) }, e)));
|
||||
return Promise.all(toDisable.map(e => this._extensionEnablementService.setEnablement(e, EnablementState.Disabled)));
|
||||
})
|
||||
.then(() => {
|
||||
return runtimeExtensions;
|
||||
});
|
||||
return this._extensionEnablementService.setEnablement(extensionsToDisable.map(e => toExtension(e)), EnablementState.Disabled)
|
||||
.then(() => runtimeExtensions);
|
||||
} else {
|
||||
return runtimeExtensions;
|
||||
}
|
||||
@@ -417,9 +715,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
|
||||
private _updateEnableProposedApi(extension: IExtensionDescription, enableProposedApiForAll: boolean, enableProposedApiFor: string | string[]): IExtensionDescription {
|
||||
if (isNonEmptyArray(product.extensionAllowedProposedApi)
|
||||
&& product.extensionAllowedProposedApi.indexOf(extension.id) >= 0
|
||||
) {
|
||||
if (allowProposedApiFromProduct(extension.identifier)) {
|
||||
// fast lane -> proposed api is available to all extensions
|
||||
// that are listed in product.json-files
|
||||
extension.enableProposedApi = true;
|
||||
@@ -427,29 +723,30 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
} else if (extension.enableProposedApi && !extension.isBuiltin) {
|
||||
if (
|
||||
!enableProposedApiForAll &&
|
||||
enableProposedApiFor.indexOf(extension.id) < 0
|
||||
enableProposedApiFor.indexOf(extension.identifier.value.toLowerCase()) < 0
|
||||
) {
|
||||
extension.enableProposedApi = false;
|
||||
console.error(`Extension '${extension.id} cannot use PROPOSED API (must started out of dev or enabled via --enable-proposed-api)`);
|
||||
console.error(`Extension '${extension.identifier.value} 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.`);
|
||||
console.warn(`Extension '${extension.identifier.value}' uses PROPOSED API which is subject to change and removal without notice.`);
|
||||
}
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
private _handleExtensionPointMessage(msg: IMessage) {
|
||||
const extensionKey = ExtensionIdentifier.toKey(msg.extensionId);
|
||||
|
||||
if (!this._extensionsMessages[msg.extensionId]) {
|
||||
this._extensionsMessages[msg.extensionId] = [];
|
||||
if (!this._extensionsMessages.has(extensionKey)) {
|
||||
this._extensionsMessages.set(extensionKey, []);
|
||||
}
|
||||
this._extensionsMessages[msg.extensionId].push(msg);
|
||||
this._extensionsMessages.get(extensionKey).push(msg);
|
||||
|
||||
const extension = this._registry.getExtensionDescription(msg.extensionId);
|
||||
const strMsg = `[${msg.extensionId}]: ${msg.message}`;
|
||||
const strMsg = `[${msg.extensionId.value}]: ${msg.message}`;
|
||||
if (extension && extension.isUnderDevelopment) {
|
||||
// This message is about the extension currently being developed
|
||||
this._showMessageToUser(msg.type, strMsg);
|
||||
@@ -468,7 +765,7 @@ export class ExtensionService extends Disposable implements IExtensionService {
|
||||
}
|
||||
*/
|
||||
this._telemetryService.publicLog('extensionsMessage', {
|
||||
type, extensionId, extensionPointId, message
|
||||
type, extensionId: extensionId.value, extensionPointId, message
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -518,24 +815,30 @@ 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);
|
||||
public _onWillActivateExtension(extensionId: ExtensionIdentifier): void {
|
||||
this._extensionHostActiveExtensions.set(ExtensionIdentifier.toKey(extensionId), extensionId);
|
||||
}
|
||||
|
||||
public _onDidActivateExtension(extensionId: ExtensionIdentifier, startup: boolean, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationEvent: string): void {
|
||||
this._extensionHostProcessActivationTimes.set(ExtensionIdentifier.toKey(extensionId), new ActivationTimes(startup, codeLoadingTime, activateCallTime, activateResolvedTime, activationEvent));
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
|
||||
public _onExtensionRuntimeError(extensionId: string, err: Error): void {
|
||||
if (!this._extensionHostExtensionRuntimeErrors[extensionId]) {
|
||||
this._extensionHostExtensionRuntimeErrors[extensionId] = [];
|
||||
public _onExtensionRuntimeError(extensionId: ExtensionIdentifier, err: Error): void {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extensionId);
|
||||
if (!this._extensionHostExtensionRuntimeErrors.has(extensionKey)) {
|
||||
this._extensionHostExtensionRuntimeErrors.set(extensionKey, []);
|
||||
}
|
||||
this._extensionHostExtensionRuntimeErrors[extensionId].push(err);
|
||||
this._extensionHostExtensionRuntimeErrors.get(extensionKey).push(err);
|
||||
this._onDidChangeExtensionsStatus.fire([extensionId]);
|
||||
}
|
||||
|
||||
public _addMessage(extensionId: string, severity: Severity, message: string): void {
|
||||
if (!this._extensionsMessages[extensionId]) {
|
||||
this._extensionsMessages[extensionId] = [];
|
||||
public _addMessage(extensionId: ExtensionIdentifier, severity: Severity, message: string): void {
|
||||
const extensionKey = ExtensionIdentifier.toKey(extensionId);
|
||||
if (!this._extensionsMessages.has(extensionKey)) {
|
||||
this._extensionsMessages.set(extensionKey, []);
|
||||
}
|
||||
this._extensionsMessages[extensionId].push({
|
||||
this._extensionsMessages.get(extensionKey).push({
|
||||
type: severity,
|
||||
message: message,
|
||||
extensionId: null,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { EnablementState, IExtensionEnablementService, IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
@@ -17,6 +16,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag
|
||||
import { IURLHandler, IURLService } from 'vs/platform/url/common/url';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const THIRTY_SECONDS = 30 * 1000;
|
||||
@@ -30,8 +30,8 @@ export const IExtensionUrlHandler = createDecorator<IExtensionUrlHandler>('inact
|
||||
|
||||
export interface IExtensionUrlHandler {
|
||||
readonly _serviceBrand: any;
|
||||
registerExtensionHandler(extensionId: string, handler: IURLHandler): void;
|
||||
unregisterExtensionHandler(extensionId: string): void;
|
||||
registerExtensionHandler(extensionId: ExtensionIdentifier, handler: IURLHandler): void;
|
||||
unregisterExtensionHandler(extensionId: ExtensionIdentifier): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,14 +53,14 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
|
||||
|
||||
constructor(
|
||||
@IURLService urlService: IURLService,
|
||||
@IExtensionService private extensionService: IExtensionService,
|
||||
@IDialogService private dialogService: IDialogService,
|
||||
@INotificationService private notificationService: INotificationService,
|
||||
@IExtensionManagementService private extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService,
|
||||
@IWindowService private windowService: IWindowService,
|
||||
@IExtensionGalleryService private galleryService: IExtensionGalleryService,
|
||||
@IStorageService private storageService: IStorageService
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService,
|
||||
@IWindowService private readonly windowService: IWindowService,
|
||||
@IExtensionGalleryService private readonly galleryService: IExtensionGalleryService,
|
||||
@IStorageService private readonly storageService: IStorageService
|
||||
) {
|
||||
const interval = setInterval(() => this.garbageCollect(), THIRTY_SECONDS);
|
||||
const urlToHandleValue = this.storageService.get(URL_TO_HANDLE, StorageScope.WORKSPACE);
|
||||
@@ -75,87 +75,80 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
|
||||
]);
|
||||
}
|
||||
|
||||
handleURL(uri: URI, confirmed?: boolean): TPromise<boolean> {
|
||||
async handleURL(uri: URI, confirmed?: boolean): Promise<boolean> {
|
||||
if (!isExtensionId(uri.authority)) {
|
||||
return TPromise.as(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionId = uri.authority;
|
||||
const wasHandlerAvailable = this.extensionHandlers.has(extensionId);
|
||||
const wasHandlerAvailable = this.extensionHandlers.has(ExtensionIdentifier.toKey(extensionId));
|
||||
const extension = await this.extensionService.getExtension(extensionId);
|
||||
|
||||
return this.extensionService.getExtension(extensionId).then(extension => {
|
||||
if (!extension) {
|
||||
await this.handleUnhandledURL(uri, { id: extensionId });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!extension) {
|
||||
return this.handleUnhandledURL(uri, { id: extensionId }).then(() => false);
|
||||
}
|
||||
|
||||
const handleURL = () => {
|
||||
const handler = this.extensionHandlers.get(extensionId);
|
||||
if (handler) {
|
||||
if (!wasHandlerAvailable) {
|
||||
// forward it directly
|
||||
return handler.handleURL(uri);
|
||||
}
|
||||
|
||||
// let the ExtensionUrlHandler instance handle this
|
||||
return TPromise.as(false);
|
||||
}
|
||||
|
||||
// collect URI for eventual extension activation
|
||||
const timestamp = new Date().getTime();
|
||||
let uris = this.uriBuffer.get(extensionId);
|
||||
|
||||
if (!uris) {
|
||||
uris = [];
|
||||
this.uriBuffer.set(extensionId, uris);
|
||||
}
|
||||
|
||||
uris.push({ timestamp, uri });
|
||||
|
||||
// activate the extension
|
||||
return this.extensionService.activateByEvent(`onUri:${extensionId}`)
|
||||
.then(() => true);
|
||||
};
|
||||
|
||||
if (confirmed) {
|
||||
return handleURL();
|
||||
}
|
||||
|
||||
return this.dialogService.confirm({
|
||||
if (!confirmed) {
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('confirmUrl', "Allow an extension to open this URL?", extensionId),
|
||||
detail: `${extension.displayName || extension.name} (${extensionId}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('open', "&&Open"),
|
||||
type: 'question'
|
||||
}).then(result => {
|
||||
|
||||
if (!result.confirmed) {
|
||||
return TPromise.as(true);
|
||||
}
|
||||
|
||||
return handleURL();
|
||||
});
|
||||
});
|
||||
|
||||
if (!result.confirmed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const handler = this.extensionHandlers.get(ExtensionIdentifier.toKey(extensionId));
|
||||
|
||||
if (handler) {
|
||||
if (!wasHandlerAvailable) {
|
||||
// forward it directly
|
||||
return await handler.handleURL(uri);
|
||||
}
|
||||
|
||||
// let the ExtensionUrlHandler instance handle this
|
||||
return false;
|
||||
}
|
||||
|
||||
// collect URI for eventual extension activation
|
||||
const timestamp = new Date().getTime();
|
||||
let uris = this.uriBuffer.get(ExtensionIdentifier.toKey(extensionId));
|
||||
|
||||
if (!uris) {
|
||||
uris = [];
|
||||
this.uriBuffer.set(ExtensionIdentifier.toKey(extensionId), uris);
|
||||
}
|
||||
|
||||
uris.push({ timestamp, uri });
|
||||
|
||||
// activate the extension
|
||||
await this.extensionService.activateByEvent(`onUri:${ExtensionIdentifier.toKey(extensionId)}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
registerExtensionHandler(extensionId: string, handler: IURLHandler): void {
|
||||
this.extensionHandlers.set(extensionId, handler);
|
||||
registerExtensionHandler(extensionId: ExtensionIdentifier, handler: IURLHandler): void {
|
||||
this.extensionHandlers.set(ExtensionIdentifier.toKey(extensionId), handler);
|
||||
|
||||
const uris = this.uriBuffer.get(extensionId) || [];
|
||||
const uris = this.uriBuffer.get(ExtensionIdentifier.toKey(extensionId)) || [];
|
||||
|
||||
for (const { uri } of uris) {
|
||||
handler.handleURL(uri);
|
||||
}
|
||||
|
||||
this.uriBuffer.delete(extensionId);
|
||||
this.uriBuffer.delete(ExtensionIdentifier.toKey(extensionId));
|
||||
}
|
||||
|
||||
unregisterExtensionHandler(extensionId: string): void {
|
||||
this.extensionHandlers.delete(extensionId);
|
||||
unregisterExtensionHandler(extensionId: ExtensionIdentifier): void {
|
||||
this.extensionHandlers.delete(ExtensionIdentifier.toKey(extensionId));
|
||||
}
|
||||
|
||||
private async handleUnhandledURL(uri: URI, extensionIdentifier: IExtensionIdentifier): Promise<void> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const extension = installedExtensions.filter(e => areSameExtensions(e.galleryIdentifier, extensionIdentifier))[0];
|
||||
const extension = installedExtensions.filter(e => areSameExtensions(e.identifier, extensionIdentifier))[0];
|
||||
|
||||
// Extension is installed
|
||||
if (extension) {
|
||||
@@ -163,88 +156,91 @@ export class ExtensionUrlHandler implements IExtensionUrlHandler, IURLHandler {
|
||||
|
||||
// Extension is not running. Reload the window to handle.
|
||||
if (enabled) {
|
||||
this.dialogService.confirm({
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('reloadAndHandle', "Extension '{0}' is not loaded. Would you like to reload the window to load the extension and open the URL?", extension.manifest.displayName || extension.manifest.name),
|
||||
detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('reloadAndOpen', "&&Reload Window and Open"),
|
||||
type: 'question'
|
||||
}).then(result => {
|
||||
if (result.confirmed) {
|
||||
return this.reloadAndHandle(uri);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.reloadAndHandle(uri);
|
||||
}
|
||||
|
||||
// Extension is disabled. Enable the extension and reload the window to handle.
|
||||
else {
|
||||
this.dialogService.confirm({
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('enableAndHandle', "Extension '{0}' is disabled. Would you like to enable the extension and reload the window to open the URL?", extension.manifest.displayName || extension.manifest.name),
|
||||
detail: `${extension.manifest.displayName || extension.manifest.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('enableAndReload', "&&Enable and Open"),
|
||||
type: 'question'
|
||||
}).then((result): TPromise<void> | null => {
|
||||
if (result.confirmed) {
|
||||
return this.extensionEnablementService.setEnablement(extension, EnablementState.Enabled)
|
||||
.then(() => this.reloadAndHandle(uri));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.extensionEnablementService.setEnablement([extension], EnablementState.Enabled);
|
||||
await this.reloadAndHandle(uri);
|
||||
}
|
||||
}
|
||||
|
||||
// Extension is not installed
|
||||
else {
|
||||
const galleryExtension = await this.galleryService.getExtension(extensionIdentifier);
|
||||
if (galleryExtension) {
|
||||
// Install the Extension and reload the window to handle.
|
||||
this.dialogService.confirm({
|
||||
message: localize('installAndHandle', "Extension '{0}' is not installed. Would you like to install the extension and reload the window to open this URL?", galleryExtension.displayName || galleryExtension.name),
|
||||
detail: `${galleryExtension.displayName || galleryExtension.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('install', "&&Install"),
|
||||
type: 'question'
|
||||
}).then(async result => {
|
||||
if (result.confirmed) {
|
||||
let notificationHandle: INotificationHandle | null = this.notificationService.notify({ severity: Severity.Info, message: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) });
|
||||
notificationHandle.progress.infinite();
|
||||
notificationHandle.onDidClose(() => notificationHandle = null);
|
||||
try {
|
||||
await this.extensionManagementService.installFromGallery(galleryExtension);
|
||||
const reloadMessage = localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString());
|
||||
const reloadActionLabel = localize('Reload', "Reload Window and Open");
|
||||
if (notificationHandle) {
|
||||
notificationHandle.progress.done();
|
||||
notificationHandle.updateMessage(reloadMessage);
|
||||
notificationHandle.updateActions({
|
||||
primary: [new Action('reloadWindow', reloadActionLabel, undefined, true, () => this.reloadAndHandle(uri))]
|
||||
});
|
||||
} else {
|
||||
this.notificationService.prompt(Severity.Info, reloadMessage,
|
||||
[{
|
||||
label: reloadActionLabel,
|
||||
run: () => this.reloadAndHandle(uri)
|
||||
}],
|
||||
{ sticky: true }
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (notificationHandle) {
|
||||
notificationHandle.progress.done();
|
||||
notificationHandle.updateSeverity(Severity.Error);
|
||||
notificationHandle.updateMessage(e);
|
||||
} else {
|
||||
this.notificationService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const galleryExtension = await this.galleryService.getCompatibleExtension(extensionIdentifier);
|
||||
|
||||
if (!galleryExtension) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Install the Extension and reload the window to handle.
|
||||
const result = await this.dialogService.confirm({
|
||||
message: localize('installAndHandle', "Extension '{0}' is not installed. Would you like to install the extension and reload the window to open this URL?", galleryExtension.displayName || galleryExtension.name),
|
||||
detail: `${galleryExtension.displayName || galleryExtension.name} (${extensionIdentifier.id}) wants to open a URL:\n\n${uri.toString()}`,
|
||||
primaryButton: localize('install', "&&Install"),
|
||||
type: 'question'
|
||||
});
|
||||
|
||||
if (!result.confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let notificationHandle: INotificationHandle | null = this.notificationService.notify({ severity: Severity.Info, message: localize('Installing', "Installing Extension '{0}'...", galleryExtension.displayName || galleryExtension.name) });
|
||||
notificationHandle.progress.infinite();
|
||||
notificationHandle.onDidClose(() => notificationHandle = null);
|
||||
|
||||
try {
|
||||
await this.extensionManagementService.installFromGallery(galleryExtension);
|
||||
const reloadMessage = localize('reload', "Would you like to reload the window and open the URL '{0}'?", uri.toString());
|
||||
const reloadActionLabel = localize('Reload', "Reload Window and Open");
|
||||
|
||||
if (notificationHandle) {
|
||||
notificationHandle.progress.done();
|
||||
notificationHandle.updateMessage(reloadMessage);
|
||||
notificationHandle.updateActions({
|
||||
primary: [new Action('reloadWindow', reloadActionLabel, undefined, true, () => this.reloadAndHandle(uri))]
|
||||
});
|
||||
} else {
|
||||
this.notificationService.prompt(Severity.Info, reloadMessage, [{ label: reloadActionLabel, run: () => this.reloadAndHandle(uri) }], { sticky: true });
|
||||
}
|
||||
} catch (e) {
|
||||
if (notificationHandle) {
|
||||
notificationHandle.progress.done();
|
||||
notificationHandle.updateSeverity(Severity.Error);
|
||||
notificationHandle.updateMessage(e);
|
||||
} else {
|
||||
this.notificationService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private reloadAndHandle(url: URI): TPromise<void> {
|
||||
private async reloadAndHandle(url: URI): Promise<void> {
|
||||
this.storageService.store(URL_TO_HANDLE, JSON.stringify(url.toJSON()), StorageScope.WORKSPACE);
|
||||
return this.windowService.reloadWindow();
|
||||
await this.windowService.reloadWindow();
|
||||
}
|
||||
|
||||
// forget about all uris buffered more than 5 minutes ago
|
||||
|
||||
@@ -30,7 +30,7 @@ export class RuntimeExtensionsInput extends EditorInput {
|
||||
return true;
|
||||
}
|
||||
|
||||
resolve(): Thenable<any> {
|
||||
resolve(): Promise<any> {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
const hasOwnProperty = Object.hasOwnProperty;
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
|
||||
export class ExtensionDescriptionRegistry {
|
||||
private readonly _onDidChange = new Emitter<void>();
|
||||
public readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
private _extensionDescriptions: IExtensionDescription[];
|
||||
private _extensionsMap: { [extensionId: string]: IExtensionDescription; };
|
||||
private _extensionsMap: Map<string, IExtensionDescription>;
|
||||
private _extensionsArr: IExtensionDescription[];
|
||||
private _activationMap: { [activationEvent: string]: IExtensionDescription[]; };
|
||||
private _activationMap: Map<string, IExtensionDescription[]>;
|
||||
|
||||
constructor(extensionDescriptions: IExtensionDescription[]) {
|
||||
this._extensionDescriptions = extensionDescriptions;
|
||||
@@ -19,64 +22,68 @@ export class ExtensionDescriptionRegistry {
|
||||
}
|
||||
|
||||
private _initialize(): void {
|
||||
this._extensionsMap = {};
|
||||
this._extensionsMap = new Map<string, IExtensionDescription>();
|
||||
this._extensionsArr = [];
|
||||
this._activationMap = {};
|
||||
this._activationMap = new Map<string, IExtensionDescription[]>();
|
||||
|
||||
for (let i = 0, len = this._extensionDescriptions.length; i < len; i++) {
|
||||
let extensionDescription = this._extensionDescriptions[i];
|
||||
|
||||
if (hasOwnProperty.call(this._extensionsMap, extensionDescription.id)) {
|
||||
for (const extensionDescription of this._extensionDescriptions) {
|
||||
if (this._extensionsMap.has(ExtensionIdentifier.toKey(extensionDescription.identifier))) {
|
||||
// No overwriting allowed!
|
||||
console.error('Extension `' + extensionDescription.id + '` is already registered');
|
||||
console.error('Extension `' + extensionDescription.identifier.value + '` is already registered');
|
||||
continue;
|
||||
}
|
||||
|
||||
this._extensionsMap[extensionDescription.id] = extensionDescription;
|
||||
this._extensionsMap.set(ExtensionIdentifier.toKey(extensionDescription.identifier), extensionDescription);
|
||||
this._extensionsArr.push(extensionDescription);
|
||||
|
||||
if (Array.isArray(extensionDescription.activationEvents)) {
|
||||
for (let j = 0, lenJ = extensionDescription.activationEvents.length; j < lenJ; j++) {
|
||||
let activationEvent = extensionDescription.activationEvents[j];
|
||||
|
||||
for (let activationEvent of extensionDescription.activationEvents) {
|
||||
// TODO@joao: there's no easy way to contribute this
|
||||
if (activationEvent === 'onUri') {
|
||||
activationEvent = `onUri:${extensionDescription.id}`;
|
||||
activationEvent = `onUri:${ExtensionIdentifier.toKey(extensionDescription.identifier)}`;
|
||||
}
|
||||
|
||||
this._activationMap[activationEvent] = this._activationMap[activationEvent] || [];
|
||||
this._activationMap[activationEvent].push(extensionDescription);
|
||||
if (!this._activationMap.has(activationEvent)) {
|
||||
this._activationMap.set(activationEvent, []);
|
||||
}
|
||||
this._activationMap.get(activationEvent)!.push(extensionDescription);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public keepOnly(extensionIds: string[]): void {
|
||||
let toKeep = new Set<string>();
|
||||
extensionIds.forEach(extensionId => toKeep.add(extensionId));
|
||||
this._extensionDescriptions = this._extensionDescriptions.filter(extension => toKeep.has(extension.id));
|
||||
public keepOnly(extensionIds: ExtensionIdentifier[]): void {
|
||||
const toKeep = new Set<string>();
|
||||
extensionIds.forEach(extensionId => toKeep.add(ExtensionIdentifier.toKey(extensionId)));
|
||||
this._extensionDescriptions = this._extensionDescriptions.filter(extension => toKeep.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
this._initialize();
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
public deltaExtensions(toAdd: IExtensionDescription[], toRemove: ExtensionIdentifier[]) {
|
||||
this._extensionDescriptions = this._extensionDescriptions.concat(toAdd);
|
||||
const toRemoveSet = new Set<string>();
|
||||
toRemove.forEach(extensionId => toRemoveSet.add(ExtensionIdentifier.toKey(extensionId)));
|
||||
this._extensionDescriptions = this._extensionDescriptions.filter(extension => !toRemoveSet.has(ExtensionIdentifier.toKey(extension.identifier)));
|
||||
this._initialize();
|
||||
this._onDidChange.fire(undefined);
|
||||
}
|
||||
|
||||
public containsActivationEvent(activationEvent: string): boolean {
|
||||
return hasOwnProperty.call(this._activationMap, activationEvent);
|
||||
return this._activationMap.has(activationEvent);
|
||||
}
|
||||
|
||||
public getExtensionDescriptionsForActivationEvent(activationEvent: string): IExtensionDescription[] {
|
||||
if (!hasOwnProperty.call(this._activationMap, activationEvent)) {
|
||||
return [];
|
||||
}
|
||||
return this._activationMap[activationEvent].slice(0);
|
||||
const extensions = this._activationMap.get(activationEvent);
|
||||
return extensions ? extensions.slice(0) : [];
|
||||
}
|
||||
|
||||
public getAllExtensionDescriptions(): IExtensionDescription[] {
|
||||
return this._extensionsArr.slice(0);
|
||||
}
|
||||
|
||||
public getExtensionDescription(extensionId: string): IExtensionDescription | null {
|
||||
if (!hasOwnProperty.call(this._extensionsMap, extensionId)) {
|
||||
return null;
|
||||
}
|
||||
return this._extensionsMap[extensionId];
|
||||
public getExtensionDescription(extensionId: ExtensionIdentifier | string): IExtensionDescription | null {
|
||||
const extension = this._extensionsMap.get(ExtensionIdentifier.toKey(extensionId));
|
||||
return extension ? extension : null;
|
||||
}
|
||||
}
|
||||
|
||||
162
src/vs/workbench/services/extensions/node/extensionHostMain.ts
Normal file
162
src/vs/workbench/services/extensions/node/extensionHostMain.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Counter } from 'vs/base/common/numbers';
|
||||
import { URI, setUriThrowOnMissingScheme } from 'vs/base/common/uri';
|
||||
import { IURITransformer } from 'vs/base/common/uriIpc';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { IEnvironment, IInitData, MainContext, MainThreadConsoleShape } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { ExtHostConfiguration } from 'vs/workbench/api/node/extHostConfiguration';
|
||||
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
|
||||
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
|
||||
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { RPCProtocol } from 'vs/workbench/services/extensions/node/rpcProtocol';
|
||||
|
||||
// we don't (yet) throw when extensions parse
|
||||
// uris that have no scheme
|
||||
setUriThrowOnMissingScheme(false);
|
||||
|
||||
const nativeExit = process.exit.bind(process);
|
||||
function patchProcess(allowExit: boolean) {
|
||||
process.exit = function (code?: number) {
|
||||
if (allowExit) {
|
||||
exit(code);
|
||||
} else {
|
||||
const err = new Error('An extension called process.exit() and this was prevented.');
|
||||
console.warn(err.stack);
|
||||
}
|
||||
} as (code?: number) => never;
|
||||
|
||||
process.crash = function () {
|
||||
const err = new Error('An extension called process.crash() and this was prevented.');
|
||||
console.warn(err.stack);
|
||||
};
|
||||
}
|
||||
|
||||
export function exit(code?: number) {
|
||||
nativeExit(code);
|
||||
}
|
||||
|
||||
export class ExtensionHostMain {
|
||||
|
||||
|
||||
private _isTerminating: boolean;
|
||||
private readonly _environment: IEnvironment;
|
||||
private readonly _extensionService: ExtHostExtensionService;
|
||||
private readonly _extHostConfiguration: ExtHostConfiguration;
|
||||
private readonly _extHostLogService: ExtHostLogService;
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
private _searchRequestIdProvider: Counter;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, initData: IInitData) {
|
||||
this._isTerminating = false;
|
||||
const uriTransformer: IURITransformer = null;
|
||||
const rpcProtocol = new RPCProtocol(protocol, null, uriTransformer);
|
||||
|
||||
// ensure URIs are transformed and revived
|
||||
initData = this.transform(initData, rpcProtocol);
|
||||
this._environment = initData.environment;
|
||||
|
||||
const allowExit = !!this._environment.extensionTestsPath; // to support other test frameworks like Jasmin that use process.exit (https://github.com/Microsoft/vscode/issues/37708)
|
||||
patchProcess(allowExit);
|
||||
|
||||
this._patchPatchedConsole(rpcProtocol.getProxy(MainContext.MainThreadConsole));
|
||||
|
||||
// services
|
||||
this._extHostLogService = new ExtHostLogService(initData.logLevel, initData.logsLocation.fsPath);
|
||||
this.disposables.push(this._extHostLogService);
|
||||
|
||||
this._searchRequestIdProvider = new Counter();
|
||||
const extHostWorkspace = new ExtHostWorkspace(rpcProtocol, initData.workspace, this._extHostLogService, this._searchRequestIdProvider);
|
||||
|
||||
this._extHostLogService.info('extension host started');
|
||||
this._extHostLogService.trace('initData', initData);
|
||||
|
||||
this._extHostConfiguration = new ExtHostConfiguration(rpcProtocol.getProxy(MainContext.MainThreadConfiguration), extHostWorkspace);
|
||||
this._extensionService = new ExtHostExtensionService(nativeExit, initData, rpcProtocol, extHostWorkspace, this._extHostConfiguration, this._extHostLogService);
|
||||
|
||||
// error forwarding and stack trace scanning
|
||||
Error.stackTraceLimit = 100; // increase number of stack frames (from 10, https://github.com/v8/v8/wiki/Stack-Trace-API)
|
||||
const extensionErrors = new WeakMap<Error, IExtensionDescription>();
|
||||
this._extensionService.getExtensionPathIndex().then(map => {
|
||||
(<any>Error).prepareStackTrace = (error: Error, stackTrace: errors.V8CallSite[]) => {
|
||||
let stackTraceMessage = '';
|
||||
let extension: IExtensionDescription;
|
||||
let fileName: string;
|
||||
for (const call of stackTrace) {
|
||||
stackTraceMessage += `\n\tat ${call.toString()}`;
|
||||
fileName = call.getFileName();
|
||||
if (!extension && fileName) {
|
||||
extension = map.findSubstr(fileName);
|
||||
}
|
||||
|
||||
}
|
||||
extensionErrors.set(error, extension);
|
||||
return `${error.name || 'Error'}: ${error.message || ''}${stackTraceMessage}`;
|
||||
};
|
||||
});
|
||||
|
||||
const mainThreadExtensions = rpcProtocol.getProxy(MainContext.MainThreadExtensionService);
|
||||
const mainThreadErrors = rpcProtocol.getProxy(MainContext.MainThreadErrors);
|
||||
errors.setUnexpectedErrorHandler(err => {
|
||||
const data = errors.transformErrorForSerialization(err);
|
||||
const extension = extensionErrors.get(err);
|
||||
if (extension) {
|
||||
mainThreadExtensions.$onExtensionRuntimeError(extension.identifier, data);
|
||||
} else {
|
||||
mainThreadErrors.$onUnexpectedError(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _patchPatchedConsole(mainThreadConsole: MainThreadConsoleShape): void {
|
||||
// The console is already patched to use `process.send()`
|
||||
const nativeProcessSend = process.send;
|
||||
process.send = (...args: any[]) => {
|
||||
if (args.length === 0 || !args[0] || args[0].type !== '__$console') {
|
||||
return nativeProcessSend.apply(process, args);
|
||||
}
|
||||
|
||||
mainThreadConsole.$logExtensionHostMessage(args[0]);
|
||||
};
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
if (this._isTerminating) {
|
||||
// we are already shutting down...
|
||||
return;
|
||||
}
|
||||
this._isTerminating = true;
|
||||
|
||||
this.disposables = dispose(this.disposables);
|
||||
|
||||
errors.setUnexpectedErrorHandler((err) => {
|
||||
// TODO: write to log once we have one
|
||||
});
|
||||
|
||||
const extensionsDeactivated = this._extensionService.deactivateAll();
|
||||
|
||||
// Give extensions 1 second to wrap up any async dispose, then exit in at most 4 seconds
|
||||
setTimeout(() => {
|
||||
Promise.race([timeout(4000), extensionsDeactivated]).then(() => exit(), () => exit());
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private transform(initData: IInitData, rpcProtocol: RPCProtocol): IInitData {
|
||||
initData.extensions.forEach((ext) => (<any>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)));
|
||||
initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot));
|
||||
initData.environment.appSettingsHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appSettingsHome));
|
||||
initData.environment.extensionDevelopmentLocationURI = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.extensionDevelopmentLocationURI));
|
||||
initData.environment.globalStorageHome = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.globalStorageHome));
|
||||
initData.logsLocation = URI.revive(rpcProtocol.transformIncomingURIs(initData.logsLocation));
|
||||
initData.workspace = rpcProtocol.transformIncomingURIs(initData.workspace);
|
||||
return initData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nativeWatchdog from 'native-watchdog';
|
||||
import { createConnection } from 'net';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { Protocol } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import product from 'vs/platform/node/product';
|
||||
import { IInitData } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/common/extensionHostProtocol';
|
||||
import { exit, ExtensionHostMain } from 'vs/workbench/services/extensions/node/extensionHostMain';
|
||||
|
||||
// With Electron 2.x and node.js 8.x the "natives" module
|
||||
// can cause a native crash (see https://github.com/nodejs/node/issues/19891 and
|
||||
// https://github.com/electron/electron/issues/10905). To prevent this from
|
||||
// happening we essentially blocklist this module from getting loaded in any
|
||||
// extension by patching the node require() function.
|
||||
(function () {
|
||||
const Module = require.__$__nodeRequire('module') as any;
|
||||
const originalLoad = Module._load;
|
||||
|
||||
Module._load = function (request) {
|
||||
if (request === 'natives') {
|
||||
throw new Error('Either the extension or a NPM dependency is using the "natives" node module which is unsupported as it can cause a crash of the extension host. Click [here](https://go.microsoft.com/fwlink/?linkid=871887) to find out more');
|
||||
}
|
||||
|
||||
return originalLoad.apply(this, arguments);
|
||||
};
|
||||
})();
|
||||
|
||||
interface IRendererConnection {
|
||||
protocol: IMessagePassingProtocol;
|
||||
initData: IInitData;
|
||||
}
|
||||
|
||||
// This calls exit directly in case the initialization is not finished and we need to exit
|
||||
// Otherwise, if initialization completed we go to extensionHostMain.terminate()
|
||||
let onTerminate = function () {
|
||||
exit();
|
||||
};
|
||||
|
||||
function createExtHostProtocol(): Promise<IMessagePassingProtocol> {
|
||||
|
||||
const pipeName = process.env.VSCODE_IPC_HOOK_EXTHOST;
|
||||
|
||||
return new Promise<IMessagePassingProtocol>((resolve, reject) => {
|
||||
|
||||
const socket = createConnection(pipeName, () => {
|
||||
socket.removeListener('error', reject);
|
||||
resolve(new Protocol(socket));
|
||||
});
|
||||
socket.once('error', reject);
|
||||
|
||||
}).then(protocol => {
|
||||
|
||||
return new class implements IMessagePassingProtocol {
|
||||
|
||||
private _terminating = false;
|
||||
|
||||
readonly onMessage: Event<any> = Event.filter(protocol.onMessage, msg => {
|
||||
if (!isMessageOfType(msg, MessageType.Terminate)) {
|
||||
return true;
|
||||
}
|
||||
this._terminating = true;
|
||||
onTerminate();
|
||||
return false;
|
||||
});
|
||||
|
||||
send(msg: any): void {
|
||||
if (!this._terminating) {
|
||||
protocol.send(msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function connectToRenderer(protocol: IMessagePassingProtocol): Promise<IRendererConnection> {
|
||||
return new Promise<IRendererConnection>((c, e) => {
|
||||
|
||||
// Listen init data message
|
||||
const first = protocol.onMessage(raw => {
|
||||
first.dispose();
|
||||
|
||||
const initData = <IInitData>JSON.parse(raw.toString());
|
||||
|
||||
const rendererCommit = initData.commit;
|
||||
const myCommit = product.commit;
|
||||
|
||||
if (rendererCommit && myCommit) {
|
||||
// Running in the built version where commits are defined
|
||||
if (rendererCommit !== myCommit) {
|
||||
exit(55);
|
||||
}
|
||||
}
|
||||
|
||||
// Print a console message when rejection isn't handled within N seconds. For details:
|
||||
// see https://nodejs.org/api/process.html#process_event_unhandledrejection
|
||||
// and https://nodejs.org/api/process.html#process_event_rejectionhandled
|
||||
const unhandledPromises: Promise<any>[] = [];
|
||||
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
|
||||
unhandledPromises.push(promise);
|
||||
setTimeout(() => {
|
||||
const idx = unhandledPromises.indexOf(promise);
|
||||
if (idx >= 0) {
|
||||
unhandledPromises.splice(idx, 1);
|
||||
console.warn('rejected promise not handled within 1 second');
|
||||
onUnexpectedError(reason);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
process.on('rejectionHandled', (promise: Promise<any>) => {
|
||||
const idx = unhandledPromises.indexOf(promise);
|
||||
if (idx >= 0) {
|
||||
unhandledPromises.splice(idx, 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Print a console message when an exception isn't handled.
|
||||
process.on('uncaughtException', function (err: Error) {
|
||||
onUnexpectedError(err);
|
||||
});
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
process.on('SIGPIPE', () => {
|
||||
onUnexpectedError(new Error('Unexpected SIGPIPE'));
|
||||
});
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
|
||||
// Kill oneself if one's parent dies. Much drama.
|
||||
setInterval(function () {
|
||||
try {
|
||||
process.kill(initData.parentPid, 0); // throws an exception if the main process doesn't exist anymore.
|
||||
} catch (e) {
|
||||
onTerminate();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// In certain cases, the event loop can become busy and never yield
|
||||
// e.g. while-true or process.nextTick endless loops
|
||||
// So also use the native node module to do it from a separate thread
|
||||
let watchdog: typeof nativeWatchdog;
|
||||
try {
|
||||
watchdog = require.__$__nodeRequire('native-watchdog');
|
||||
watchdog.start(initData.parentPid);
|
||||
} catch (err) {
|
||||
// no problem...
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
|
||||
// Tell the outside that we are initialized
|
||||
protocol.send(createMessageOfType(MessageType.Initialized));
|
||||
|
||||
c({ protocol, initData });
|
||||
});
|
||||
|
||||
// Tell the outside that we are ready to receive messages
|
||||
protocol.send(createMessageOfType(MessageType.Ready));
|
||||
});
|
||||
}
|
||||
|
||||
patchExecArgv();
|
||||
|
||||
createExtHostProtocol().then(protocol => {
|
||||
// connect to main side
|
||||
return connectToRenderer(protocol);
|
||||
}).then(renderer => {
|
||||
// setup things
|
||||
const extensionHostMain = new ExtensionHostMain(renderer.protocol, renderer.initData);
|
||||
onTerminate = () => extensionHostMain.terminate();
|
||||
}).catch(err => console.error(err));
|
||||
|
||||
function patchExecArgv() {
|
||||
// when encountering the prevent-inspect flag we delete this
|
||||
// and the prior flag
|
||||
if (process.env.VSCODE_PREVENT_FOREIGN_INSPECT) {
|
||||
for (let i = 0; i < process.execArgv.length; i++) {
|
||||
if (process.execArgv[i].match(/--inspect-brk=\d+|--inspect=\d+/)) {
|
||||
process.execArgv.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,28 +42,4 @@ export class ExtensionManagementServerService implements IExtensionManagementSer
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class SingleServerExtensionManagementServerService implements IExtensionManagementServerService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly extensionManagementServer: IExtensionManagementServer
|
||||
) {
|
||||
}
|
||||
|
||||
getExtensionManagementServer(location: URI): IExtensionManagementServer | null {
|
||||
const authority = location.scheme === Schemas.file ? localExtensionManagementServerAuthority : location.authority;
|
||||
return this.extensionManagementServer.authority === authority ? this.extensionManagementServer : null;
|
||||
}
|
||||
|
||||
get localExtensionManagementServer(): IExtensionManagementServer | null {
|
||||
return this.extensionManagementServer.authority === localExtensionManagementServerAuthority ? this.extensionManagementServer : null;
|
||||
}
|
||||
|
||||
get remoteExtensionManagementServer(): IExtensionManagementServer | null {
|
||||
return this.extensionManagementServer.authority !== localExtensionManagementServerAuthority ? this.extensionManagementServer : null;
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@ import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import * as pfs from 'vs/base/node/pfs';
|
||||
import { getGalleryExtensionId, getLocalExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { getIdAndVersionFromLocalExtensionId } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
|
||||
import { getGalleryExtensionId, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { isValidExtensionVersion } from 'vs/platform/extensions/node/extensionValidator';
|
||||
import { IExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, ExtensionIdentifierWithVersion } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
const MANIFEST_FILE = 'package.json';
|
||||
|
||||
@@ -304,6 +304,8 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
|
||||
// Relax the readonly properties here, it is the one place where we check and normalize values
|
||||
export interface IRelaxedExtensionDescription {
|
||||
id: string;
|
||||
uuid?: string;
|
||||
identifier: ExtensionIdentifier;
|
||||
name: string;
|
||||
version: string;
|
||||
publisher: string;
|
||||
@@ -343,6 +345,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
|
||||
|
||||
// id := `publisher.name`
|
||||
extensionDescription.id = `${extensionDescription.publisher}.${extensionDescription.name}`;
|
||||
extensionDescription.identifier = new ExtensionIdentifier(extensionDescription.id);
|
||||
|
||||
// main := absolutePath(`main`)
|
||||
if (extensionDescription.main) {
|
||||
@@ -507,7 +510,7 @@ export class ExtensionScanner {
|
||||
/**
|
||||
* Read the extension defined in `absoluteFolderPath`
|
||||
*/
|
||||
public static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise<IExtensionDescription | null> {
|
||||
private static scanExtension(version: string, log: ILog, absoluteFolderPath: string, isBuiltin: boolean, isUnderDevelopment: boolean, nlsConfig: NlsConfiguration): Promise<IExtensionDescription | null> {
|
||||
absoluteFolderPath = path.normalize(absoluteFolderPath);
|
||||
|
||||
let parser = new ExtensionManifestParser(version, log, absoluteFolderPath, isBuiltin, isUnderDevelopment);
|
||||
@@ -557,31 +560,17 @@ export class ExtensionScanner {
|
||||
refs.sort((a, b) => a.name < b.name ? -1 : 1);
|
||||
|
||||
if (!isBuiltin) {
|
||||
// TODO: align with extensionsService
|
||||
const nonGallery: IExtensionReference[] = [];
|
||||
const gallery: IExtensionReference[] = [];
|
||||
|
||||
refs.forEach(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];
|
||||
refs = refs.filter(ref => ref.name.indexOf('.') !== 0); // Do not consider user extension folder starting with `.`
|
||||
}
|
||||
|
||||
const nlsConfig = ExtensionScannerInput.createNLSConfig(input);
|
||||
let _extensionDescriptions = await Promise.all(refs.map(r => this.scanExtension(input.ourVersion, log, r.path, isBuiltin, isUnderDevelopment, nlsConfig)));
|
||||
let extensionDescriptions = arrays.coalesce(_extensionDescriptions);
|
||||
extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[getLocalExtensionId(getGalleryExtensionId(item.publisher, item.name), item.version)]);
|
||||
extensionDescriptions = extensionDescriptions.filter(item => item !== null && !obsolete[new ExtensionIdentifierWithVersion({ id: getGalleryExtensionId(item.publisher, item.name) }, item.version).key()]);
|
||||
|
||||
if (!isBuiltin) {
|
||||
// Filter out outdated extensions
|
||||
const byExtension: IExtensionDescription[][] = groupByExtension(extensionDescriptions, e => ({ id: e.id, uuid: e.uuid }));
|
||||
const byExtension: IExtensionDescription[][] = groupByExtension(extensionDescriptions, e => ({ id: e.identifier.value, uuid: e.uuid }));
|
||||
extensionDescriptions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]);
|
||||
}
|
||||
|
||||
@@ -624,15 +613,23 @@ export class ExtensionScanner {
|
||||
});
|
||||
}
|
||||
|
||||
public static scanSingleExtension(input: ExtensionScannerInput, log: ILog): Promise<IExtensionDescription | null> {
|
||||
const absoluteFolderPath = input.absoluteFolderPath;
|
||||
const isBuiltin = input.isBuiltin;
|
||||
const isUnderDevelopment = input.isUnderDevelopment;
|
||||
const nlsConfig = ExtensionScannerInput.createNLSConfig(input);
|
||||
return this.scanExtension(input.ourVersion, log, absoluteFolderPath, isBuiltin, isUnderDevelopment, nlsConfig);
|
||||
}
|
||||
|
||||
public static mergeBuiltinExtensions(builtinExtensions: Promise<IExtensionDescription[]>, extraBuiltinExtensions: Promise<IExtensionDescription[]>): Promise<IExtensionDescription[]> {
|
||||
return Promise.all([builtinExtensions, extraBuiltinExtensions]).then(([builtinExtensions, extraBuiltinExtensions]) => {
|
||||
let resultMap: { [id: string]: IExtensionDescription; } = Object.create(null);
|
||||
for (let i = 0, len = builtinExtensions.length; i < len; i++) {
|
||||
resultMap[builtinExtensions[i].id] = builtinExtensions[i];
|
||||
resultMap[ExtensionIdentifier.toKey(builtinExtensions[i].identifier)] = builtinExtensions[i];
|
||||
}
|
||||
// Overwrite with extensions found in extra
|
||||
for (let i = 0, len = extraBuiltinExtensions.length; i < len; i++) {
|
||||
resultMap[extraBuiltinExtensions[i].id] = extraBuiltinExtensions[i];
|
||||
resultMap[ExtensionIdentifier.toKey(extraBuiltinExtensions[i].identifier)] = extraBuiltinExtensions[i];
|
||||
}
|
||||
|
||||
let resultArr = Object.keys(resultMap).map((id) => resultMap[id]);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
export class LazyPromise implements Thenable<any> {
|
||||
export class LazyPromise implements Promise<any> {
|
||||
|
||||
private _actual: Promise<any> | null;
|
||||
private _actualOk: ((value?: any) => any) | null;
|
||||
@@ -78,4 +78,12 @@ export class LazyPromise implements Thenable<any> {
|
||||
public then(success: any, error: any): any {
|
||||
return this._ensureActual().then(success, error);
|
||||
}
|
||||
|
||||
public catch(error: any): any {
|
||||
return this._ensureActual().then(undefined, error);
|
||||
}
|
||||
|
||||
public finally(callback): any {
|
||||
return this._ensureActual().finally(callback);
|
||||
}
|
||||
}
|
||||
|
||||
317
src/vs/workbench/services/extensions/node/proxyResolver.ts
Normal file
317
src/vs/workbench/services/extensions/node/proxyResolver.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as nodeurl from 'url';
|
||||
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace';
|
||||
import { ExtHostConfigProvider } from 'vs/workbench/api/node/extHostConfiguration';
|
||||
import { ProxyAgent } from 'vscode-proxy-agent';
|
||||
import { MainThreadTelemetryShape } from 'vs/workbench/api/node/extHost.protocol';
|
||||
import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
interface ConnectionResult {
|
||||
proxy: string;
|
||||
connection: string;
|
||||
code: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function connectProxyResolver(
|
||||
extHostWorkspace: ExtHostWorkspace,
|
||||
configProvider: ExtHostConfigProvider,
|
||||
extensionService: ExtHostExtensionService,
|
||||
extHostLogService: ExtHostLogService,
|
||||
mainThreadTelemetry: MainThreadTelemetryShape
|
||||
) {
|
||||
const agents = createProxyAgents(extHostWorkspace, configProvider, extHostLogService, mainThreadTelemetry);
|
||||
const lookup = createPatchedModules(configProvider, agents);
|
||||
return configureModuleLoading(extensionService, lookup);
|
||||
}
|
||||
|
||||
const maxCacheEntries = 5000; // Cache can grow twice that much due to 'oldCache'.
|
||||
|
||||
function createProxyAgents(
|
||||
extHostWorkspace: ExtHostWorkspace,
|
||||
configProvider: ExtHostConfigProvider,
|
||||
extHostLogService: ExtHostLogService,
|
||||
mainThreadTelemetry: MainThreadTelemetryShape
|
||||
) {
|
||||
let settingsProxy = proxyFromConfigURL(configProvider.getConfiguration('http')
|
||||
.get<string>('proxy'));
|
||||
configProvider.onDidChangeConfiguration(e => {
|
||||
settingsProxy = proxyFromConfigURL(configProvider.getConfiguration('http')
|
||||
.get<string>('proxy'));
|
||||
});
|
||||
const env = process.env;
|
||||
let envProxy = proxyFromConfigURL(env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY); // Not standardized.
|
||||
|
||||
let cacheRolls = 0;
|
||||
let oldCache = new Map<string, string>();
|
||||
let cache = new Map<string, string>();
|
||||
function getCacheKey(url: nodeurl.UrlWithStringQuery) {
|
||||
// Expecting proxies to usually be the same per scheme://host:port. Assuming that for performance.
|
||||
return nodeurl.format({ ...url, ...{ pathname: undefined, search: undefined, hash: undefined } });
|
||||
}
|
||||
function getCachedProxy(key: string) {
|
||||
let proxy = cache.get(key);
|
||||
if (proxy) {
|
||||
return proxy;
|
||||
}
|
||||
proxy = oldCache.get(key);
|
||||
if (proxy) {
|
||||
oldCache.delete(key);
|
||||
cacheProxy(key, proxy);
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
function cacheProxy(key: string, proxy: string) {
|
||||
cache.set(key, proxy);
|
||||
if (cache.size >= maxCacheEntries) {
|
||||
oldCache = cache;
|
||||
cache = new Map();
|
||||
cacheRolls++;
|
||||
extHostLogService.trace('ProxyResolver#cacheProxy cacheRolls', cacheRolls);
|
||||
}
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timer | undefined;
|
||||
let count = 0;
|
||||
let duration = 0;
|
||||
let errorCount = 0;
|
||||
let cacheCount = 0;
|
||||
let envCount = 0;
|
||||
let settingsCount = 0;
|
||||
let localhostCount = 0;
|
||||
let results: ConnectionResult[] = [];
|
||||
function logEvent() {
|
||||
timeout = undefined;
|
||||
/* __GDPR__
|
||||
"resolveProxy" : {
|
||||
"count": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"errorCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheSize": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"cacheRolls": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"envCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"settingsCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"localhostCount": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
|
||||
"results": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
|
||||
}
|
||||
*/
|
||||
mainThreadTelemetry.$publicLog('resolveProxy', { count, duration, errorCount, cacheCount, cacheSize: cache.size, cacheRolls, envCount, settingsCount, localhostCount, results });
|
||||
count = duration = errorCount = cacheCount = envCount = settingsCount = localhostCount = 0;
|
||||
results = [];
|
||||
}
|
||||
|
||||
function resolveProxy(req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(logEvent, 10 * 60 * 1000);
|
||||
}
|
||||
|
||||
const parsedUrl = nodeurl.parse(url); // Coming from Node's URL, sticking with that.
|
||||
|
||||
const hostname = parsedUrl.hostname;
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' || hostname === '::ffff:127.0.0.1') {
|
||||
localhostCount++;
|
||||
callback('DIRECT');
|
||||
extHostLogService.trace('ProxyResolver#resolveProxy localhost', url, 'DIRECT');
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsProxy) {
|
||||
settingsCount++;
|
||||
callback(settingsProxy);
|
||||
extHostLogService.trace('ProxyResolver#resolveProxy settings', url, settingsProxy);
|
||||
return;
|
||||
}
|
||||
|
||||
if (envProxy) {
|
||||
envCount++;
|
||||
callback(envProxy);
|
||||
extHostLogService.trace('ProxyResolver#resolveProxy env', url, envProxy);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getCacheKey(parsedUrl);
|
||||
const proxy = getCachedProxy(key);
|
||||
if (proxy) {
|
||||
cacheCount++;
|
||||
collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req);
|
||||
callback(proxy);
|
||||
extHostLogService.trace('ProxyResolver#resolveProxy cached', url, proxy);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
extHostWorkspace.resolveProxy(url) // Use full URL to ensure it is an actually used one.
|
||||
.then(proxy => {
|
||||
cacheProxy(key, proxy);
|
||||
collectResult(results, proxy, parsedUrl.protocol === 'https:' ? 'HTTPS' : 'HTTP', req);
|
||||
callback(proxy);
|
||||
extHostLogService.debug('ProxyResolver#resolveProxy', url, proxy);
|
||||
}).then(() => {
|
||||
count++;
|
||||
duration = Date.now() - start + duration;
|
||||
}, err => {
|
||||
errorCount++;
|
||||
callback();
|
||||
extHostLogService.error('ProxyResolver#resolveProxy', toErrorMessage(err));
|
||||
});
|
||||
}
|
||||
|
||||
const httpAgent: http.Agent = new ProxyAgent({ resolveProxy });
|
||||
(<any>httpAgent).defaultPort = 80;
|
||||
const httpsAgent: http.Agent = new ProxyAgent({ resolveProxy });
|
||||
(<any>httpsAgent).defaultPort = 443;
|
||||
return { http: httpAgent, https: httpsAgent };
|
||||
}
|
||||
|
||||
function collectResult(results: ConnectionResult[], resolveProxy: string, connection: string, req: http.ClientRequest) {
|
||||
const proxy = resolveProxy ? String(resolveProxy).trim().split(/\s+/, 1)[0] : 'EMPTY';
|
||||
req.on('response', res => {
|
||||
const code = `HTTP_${res.statusCode}`;
|
||||
const result = findOrCreateResult(results, proxy, connection, code);
|
||||
result.count++;
|
||||
});
|
||||
req.on('error', err => {
|
||||
const code = err && typeof (<any>err).code === 'string' && (<any>err).code || 'UNKNOWN_ERROR';
|
||||
const result = findOrCreateResult(results, proxy, connection, code);
|
||||
result.count++;
|
||||
});
|
||||
}
|
||||
|
||||
function findOrCreateResult(results: ConnectionResult[], proxy: string, connection: string, code: string): ConnectionResult | undefined {
|
||||
for (const result of results) {
|
||||
if (result.proxy === proxy && result.connection === connection && result.code === code) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
const result = { proxy, connection, code, count: 0 };
|
||||
results.push(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function proxyFromConfigURL(configURL: string) {
|
||||
const url = (configURL || '').trim();
|
||||
const i = url.indexOf('://');
|
||||
if (i === -1) {
|
||||
return undefined;
|
||||
}
|
||||
const scheme = url.substr(0, i).toLowerCase();
|
||||
const proxy = url.substr(i + 3);
|
||||
if (scheme === 'http') {
|
||||
return 'PROXY ' + proxy;
|
||||
} else if (scheme === 'https') {
|
||||
return 'HTTPS ' + proxy;
|
||||
} else if (scheme === 'socks') {
|
||||
return 'SOCKS ' + proxy;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createPatchedModules(configProvider: ExtHostConfigProvider, agents: { http: http.Agent; https: http.Agent; }) {
|
||||
const setting = {
|
||||
config: configProvider.getConfiguration('http')
|
||||
.get<string>('proxySupport') || 'off'
|
||||
};
|
||||
configProvider.onDidChangeConfiguration(e => {
|
||||
setting.config = configProvider.getConfiguration('http')
|
||||
.get<string>('proxySupport') || 'off';
|
||||
});
|
||||
|
||||
return {
|
||||
http: {
|
||||
off: assign({}, http, patches(http, agents.http, agents.https, { config: 'off' }, true)),
|
||||
on: assign({}, http, patches(http, agents.http, agents.https, { config: 'on' }, true)),
|
||||
override: assign({}, http, patches(http, agents.http, agents.https, { config: 'override' }, true)),
|
||||
onRequest: assign({}, http, patches(http, agents.http, agents.https, setting, true)),
|
||||
default: assign(http, patches(http, agents.http, agents.https, setting, false)) // run last
|
||||
},
|
||||
https: {
|
||||
off: assign({}, https, patches(https, agents.https, agents.http, { config: 'off' }, true)),
|
||||
on: assign({}, https, patches(https, agents.https, agents.http, { config: 'on' }, true)),
|
||||
override: assign({}, https, patches(https, agents.https, agents.http, { config: 'override' }, true)),
|
||||
onRequest: assign({}, https, patches(https, agents.https, agents.http, setting, true)),
|
||||
default: assign(https, patches(https, agents.https, agents.http, setting, false)) // run last
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function patches(originals: typeof http | typeof https, agent: http.Agent, otherAgent: http.Agent, setting: { config: string; }, onRequest: boolean) {
|
||||
return {
|
||||
get: patch(originals.get),
|
||||
request: patch(originals.request)
|
||||
};
|
||||
|
||||
function patch(original: typeof http.get) {
|
||||
function patched(url: string | URL, options?: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest {
|
||||
if (typeof url !== 'string' && !(url && (<any>url).searchParams)) {
|
||||
callback = <any>options;
|
||||
options = url;
|
||||
url = null;
|
||||
}
|
||||
if (typeof options === 'function') {
|
||||
callback = options;
|
||||
options = null;
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
const config = onRequest && ((<any>options)._vscodeProxySupport || /* LS */ (<any>options)._vscodeSystemProxy) || setting.config;
|
||||
if (config === 'off') {
|
||||
return original.apply(null, arguments as unknown as any[]);
|
||||
}
|
||||
|
||||
if (!options.socketPath && (config === 'override' || config === 'on' && !options.agent) && options.agent !== agent && options.agent !== otherAgent) {
|
||||
if (url) {
|
||||
const parsed = typeof url === 'string' ? new nodeurl.URL(url) : url;
|
||||
const urlOptions = {
|
||||
protocol: parsed.protocol,
|
||||
hostname: parsed.hostname.lastIndexOf('[', 0) === 0 ? parsed.hostname.slice(1, -1) : parsed.hostname,
|
||||
port: parsed.port,
|
||||
path: `${parsed.pathname}${parsed.search}`
|
||||
};
|
||||
if (parsed.username || parsed.password) {
|
||||
options.auth = `${parsed.username}:${parsed.password}`;
|
||||
}
|
||||
options = { ...urlOptions, ...options };
|
||||
} else {
|
||||
options = { ...options };
|
||||
}
|
||||
options.agent = agent;
|
||||
return original(options, callback);
|
||||
}
|
||||
|
||||
return original.apply(null, arguments as unknown as any[]);
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
}
|
||||
|
||||
function configureModuleLoading(extensionService: ExtHostExtensionService, lookup: ReturnType<typeof createPatchedModules>): Promise<void> {
|
||||
return extensionService.getExtensionPathIndex()
|
||||
.then(extensionPaths => {
|
||||
const node_module = <any>require.__$__nodeRequire('module');
|
||||
const original = node_module._load;
|
||||
node_module._load = function load(request: string, parent: any, isMain: any) {
|
||||
if (request !== 'http' && request !== 'https') {
|
||||
return original.apply(this, arguments);
|
||||
}
|
||||
|
||||
const modules = lookup[request];
|
||||
const ext = extensionPaths.findSubstr(URI.file(parent.filename).fsPath);
|
||||
if (ext && ext.enableProposedApi) {
|
||||
return modules[(<any>ext).proxySupport] || modules.onRequest;
|
||||
}
|
||||
return modules.default;
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -9,9 +9,7 @@ import { CharCode } from 'vs/base/common/charCode';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { MarshalledObject } from 'vs/base/common/marshalling';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IURITransformer } from 'vs/base/common/uriIpc';
|
||||
import { IURITransformer, transformIncomingURIs } from 'vs/base/common/uriIpc';
|
||||
import { IMessagePassingProtocol } from 'vs/base/parts/ipc/node/ipc';
|
||||
import { LazyPromise } from 'vs/workbench/services/extensions/node/lazyPromise';
|
||||
import { IRPCProtocol, ProxyIdentifier, getStringIdentifierForProxy } from 'vs/workbench/services/extensions/node/proxyIdentifier';
|
||||
@@ -40,75 +38,6 @@ function createURIReplacer(transformer: IURITransformer | null): JSONStringifyRe
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function transformOutgoingURIs<T>(obj: T, transformer: IURITransformer): T {
|
||||
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<T>(obj: T, transformer: IURITransformer): T {
|
||||
const result = _transformIncomingURIs(obj, transformer, 0);
|
||||
if (result === null) {
|
||||
// no change
|
||||
return obj;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const enum RequestInitiator {
|
||||
LocalSide = 0,
|
||||
OtherSide = 1
|
||||
@@ -354,7 +283,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol {
|
||||
}
|
||||
const callId = String(req);
|
||||
|
||||
let promise: Thenable<any>;
|
||||
let promise: Promise<any>;
|
||||
let cancel: () => void;
|
||||
if (usesCancellationToken) {
|
||||
const cancellationTokenSource = new CancellationTokenSource();
|
||||
@@ -441,7 +370,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol {
|
||||
pendingReply.resolveErr(err);
|
||||
}
|
||||
|
||||
private _invokeHandler(rpcId: number, methodName: string, args: any[]): Thenable<any> {
|
||||
private _invokeHandler(rpcId: number, methodName: string, args: any[]): Promise<any> {
|
||||
try {
|
||||
return Promise.resolve(this._doInvokeHandler(rpcId, methodName, args));
|
||||
} catch (err) {
|
||||
@@ -461,7 +390,7 @@ export class RPCProtocol extends Disposable implements IRPCProtocol {
|
||||
return method.apply(actor, args);
|
||||
}
|
||||
|
||||
private _remoteCall(rpcId: number, methodName: string, args: any[]): Thenable<any> {
|
||||
private _remoteCall(rpcId: number, methodName: string, args: any[]): Promise<any> {
|
||||
if (this._isDisposed) {
|
||||
return Promise.reject<any>(errors.canceled());
|
||||
}
|
||||
@@ -592,7 +521,7 @@ class MessageBuffer {
|
||||
return buff;
|
||||
}
|
||||
|
||||
public static sizeMixedArray(arr: (string | Buffer)[], arrLengths: number[]): number {
|
||||
public static sizeMixedArray(arr: Array<string | Buffer>, arrLengths: number[]): number {
|
||||
let size = 0;
|
||||
size += 1; // arr length
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
@@ -608,7 +537,7 @@ class MessageBuffer {
|
||||
return size;
|
||||
}
|
||||
|
||||
public writeMixedArray(arr: (string | Buffer)[], arrLengths: number[]): void {
|
||||
public writeMixedArray(arr: Array<string | Buffer>, arrLengths: number[]): void {
|
||||
this._buff.writeUInt8(arr.length, this._offset, true); this._offset += 1;
|
||||
for (let i = 0, len = arr.length; i < len; i++) {
|
||||
const el = arr[i];
|
||||
@@ -623,9 +552,9 @@ class MessageBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
public readMixedArray(): (string | Buffer)[] {
|
||||
public readMixedArray(): Array<string | Buffer> {
|
||||
const arrLen = this._buff.readUInt8(this._offset, true); this._offset += 1;
|
||||
let arr: (string | Buffer)[] = new Array(arrLen);
|
||||
let arr: Array<string | Buffer> = new Array(arrLen);
|
||||
for (let i = 0; i < arrLen; i++) {
|
||||
const argType = <ArgType>this.readUInt8();
|
||||
if (argType === ArgType.String) {
|
||||
@@ -651,7 +580,7 @@ class MessageIO {
|
||||
|
||||
public static serializeRequest(req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean, replacer: JSONStringifyReplacer | null): Buffer {
|
||||
if (this._arrayContainsBuffer(args)) {
|
||||
let massagedArgs: (string | Buffer)[] = new Array(args.length);
|
||||
let massagedArgs: Array<string | Buffer> = new Array(args.length);
|
||||
let argsLengths: number[] = new Array(args.length);
|
||||
for (let i = 0, len = args.length; i < len; i++) {
|
||||
const arg = args[i];
|
||||
@@ -695,7 +624,7 @@ class MessageIO {
|
||||
};
|
||||
}
|
||||
|
||||
private static _requestMixedArgs(req: number, rpcId: number, method: string, args: (string | Buffer)[], argsLengths: number[], usesCancellationToken: boolean): Buffer {
|
||||
private static _requestMixedArgs(req: number, rpcId: number, method: string, args: Array<string | Buffer>, argsLengths: number[], usesCancellationToken: boolean): Buffer {
|
||||
const methodByteLength = Buffer.byteLength(method, 'utf8');
|
||||
|
||||
let len = 0;
|
||||
|
||||
@@ -15,7 +15,7 @@ suite('RPCProtocol', () => {
|
||||
class MessagePassingProtocol implements IMessagePassingProtocol {
|
||||
private _pair: MessagePassingProtocol;
|
||||
|
||||
private readonly _onMessage: Emitter<Buffer> = new Emitter<Buffer>();
|
||||
private readonly _onMessage = new Emitter<Buffer>();
|
||||
public readonly onMessage: Event<Buffer> = this._onMessage.event;
|
||||
|
||||
public setPair(other: MessagePassingProtocol) {
|
||||
@@ -32,7 +32,7 @@ suite('RPCProtocol', () => {
|
||||
let delegate: (a1: any, a2: any) => any;
|
||||
let bProxy: BClass;
|
||||
class BClass {
|
||||
$m(a1: any, a2: any): Thenable<any> {
|
||||
$m(a1: any, a2: any): Promise<any> {
|
||||
return Promise.resolve(delegate.call(null, a1, a2));
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,6 @@ suite('RPCProtocol', () => {
|
||||
let A = new RPCProtocol(a_protocol);
|
||||
let B = new RPCProtocol(b_protocol);
|
||||
|
||||
delegate = null;
|
||||
|
||||
const bIdentifier = new ProxyIdentifier<BClass>(false, 'bb');
|
||||
const bInstance = new BClass();
|
||||
B.set(bIdentifier, bInstance);
|
||||
|
||||
@@ -121,8 +121,7 @@ export class ResourceEncodings extends Disposable implements IResourceEncodings
|
||||
|
||||
private getEncodingOverride(resource: uri): string | null {
|
||||
if (resource && this.encodingOverride && this.encodingOverride.length) {
|
||||
for (let i = 0; i < this.encodingOverride.length; i++) {
|
||||
const override = this.encodingOverride[i];
|
||||
for (const override of this.encodingOverride) {
|
||||
|
||||
// check if the resource is child of encoding override path
|
||||
if (override.parent && isParent(resource.fsPath, override.parent.fsPath, !isLinux /* ignorecase */)) {
|
||||
|
||||
@@ -121,7 +121,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
private handleError(error: string | Error): void {
|
||||
const msg = error ? error.toString() : void 0;
|
||||
const msg = error ? error.toString() : undefined;
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
@@ -207,7 +207,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
activateProvider(scheme: string): Thenable<void> {
|
||||
activateProvider(scheme: string): Promise<void> {
|
||||
return Promise.reject(new Error('not implemented'));
|
||||
}
|
||||
|
||||
@@ -215,20 +215,20 @@ export class FileService extends Disposable implements IFileService {
|
||||
return resource.scheme === Schemas.file;
|
||||
}
|
||||
|
||||
resolveFile(resource: uri, options?: IResolveFileOptions): Thenable<IFileStat> {
|
||||
resolveFile(resource: uri, options?: IResolveFileOptions): Promise<IFileStat> {
|
||||
return this.resolve(resource, options);
|
||||
}
|
||||
|
||||
resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): Thenable<IResolveFileResult[]> {
|
||||
resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): Promise<IResolveFileResult[]> {
|
||||
return Promise.all(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options)
|
||||
.then(stat => ({ stat, success: true }), error => ({ stat: void 0, success: false }))));
|
||||
.then(stat => ({ stat, success: true }), error => ({ stat: undefined, success: false }))));
|
||||
}
|
||||
|
||||
existsFile(resource: uri): Thenable<boolean> {
|
||||
existsFile(resource: uri): Promise<boolean> {
|
||||
return this.resolveFile(resource).then(() => true, () => false);
|
||||
}
|
||||
|
||||
resolveContent(resource: uri, options?: IResolveContentOptions): Thenable<IContent> {
|
||||
resolveContent(resource: uri, options?: IResolveContentOptions): Promise<IContent> {
|
||||
return this.resolveStreamContent(resource, options).then(streamContent => {
|
||||
return new Promise<IContent>((resolve, reject) => {
|
||||
|
||||
@@ -251,7 +251,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
resolveStreamContent(resource: uri, options?: IResolveContentOptions): Thenable<IStreamContent> {
|
||||
resolveStreamContent(resource: uri, options?: IResolveContentOptions): Promise<IStreamContent> {
|
||||
|
||||
// Guard early against attempts to resolve an invalid file path
|
||||
if (resource.scheme !== Schemas.file || !resource.fsPath) {
|
||||
@@ -263,13 +263,13 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
const result: IStreamContent = {
|
||||
resource: void 0,
|
||||
name: void 0,
|
||||
mtime: void 0,
|
||||
etag: void 0,
|
||||
encoding: void 0,
|
||||
resource: undefined,
|
||||
name: undefined,
|
||||
mtime: undefined,
|
||||
etag: undefined,
|
||||
encoding: undefined,
|
||||
isReadonly: false,
|
||||
value: void 0
|
||||
value: undefined
|
||||
};
|
||||
|
||||
const contentResolverTokenSource = new CancellationTokenSource();
|
||||
@@ -326,7 +326,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}, err => {
|
||||
|
||||
// Wrap file not found errors
|
||||
@@ -341,7 +341,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return onStatError(err);
|
||||
});
|
||||
|
||||
let completePromise: Thenable<void>;
|
||||
let completePromise: Promise<void>;
|
||||
|
||||
// await the stat iff we already have an etag so that we compare the
|
||||
// etag from the stat before we actually read the file again.
|
||||
@@ -358,8 +358,8 @@ export class FileService extends Disposable implements IFileService {
|
||||
let contentsError: Error;
|
||||
|
||||
completePromise = Promise.all([
|
||||
statsPromise.then(() => void 0, error => statsError = error),
|
||||
this.fillInContents(result, resource, options, contentResolverTokenSource.token).then(() => void 0, error => contentsError = error)
|
||||
statsPromise.then(() => undefined, error => statsError = error),
|
||||
this.fillInContents(result, resource, options, contentResolverTokenSource.token).then(() => undefined, error => contentsError = error)
|
||||
]).then(() => {
|
||||
// Since each file operation can return a FileOperationError
|
||||
// we want to prefer that one if possible. Otherwise we just
|
||||
@@ -376,7 +376,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return Promise.reject(statsError || contentsError);
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -391,19 +391,19 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
private fillInContents(content: IStreamContent, resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable<void> {
|
||||
private fillInContents(content: IStreamContent, resource: uri, options: IResolveContentOptions, token: CancellationToken): Promise<void> {
|
||||
return this.resolveFileData(resource, options, token).then(data => {
|
||||
content.encoding = data.encoding;
|
||||
content.value = data.stream;
|
||||
});
|
||||
}
|
||||
|
||||
private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable<IContentData> {
|
||||
private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): Promise<IContentData> {
|
||||
const chunkBuffer = Buffer.allocUnsafe(64 * 1024);
|
||||
|
||||
const result: IContentData = {
|
||||
encoding: void 0,
|
||||
stream: void 0
|
||||
encoding: undefined,
|
||||
stream: undefined
|
||||
};
|
||||
|
||||
return new Promise<IContentData>((resolve, reject) => {
|
||||
@@ -528,7 +528,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
resolve(result);
|
||||
handleChunk(bytesRead);
|
||||
}
|
||||
}).then(void 0, err => {
|
||||
}).then(undefined, err => {
|
||||
// failed to get encoding
|
||||
finish(err);
|
||||
});
|
||||
@@ -542,7 +542,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Thenable<IFileStat> {
|
||||
updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Promise<IFileStat> {
|
||||
if (options.writeElevated) {
|
||||
return this.doUpdateContentElevated(resource, value, options);
|
||||
}
|
||||
@@ -550,12 +550,12 @@ export class FileService extends Disposable implements IFileService {
|
||||
return this.doUpdateContent(resource, value, options);
|
||||
}
|
||||
|
||||
private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Thenable<IFileStat> {
|
||||
private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Promise<IFileStat> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
// 1.) check file for writing
|
||||
return this.checkFileBeforeWriting(absolutePath, options).then(exists => {
|
||||
let createParentsPromise: Thenable<boolean>;
|
||||
let createParentsPromise: Promise<boolean>;
|
||||
if (exists) {
|
||||
createParentsPromise = Promise.resolve();
|
||||
} else {
|
||||
@@ -565,7 +565,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
// 2.) create parents as needed
|
||||
return createParentsPromise.then(() => {
|
||||
const encodingToWrite = this._encoding.getWriteEncoding(resource, options.encoding);
|
||||
let addBomPromise: Thenable<boolean> = Promise.resolve(false);
|
||||
let addBomPromise: Promise<boolean> = Promise.resolve(false);
|
||||
|
||||
// UTF_16 BE and LE as well as UTF_8 with BOM always have a BOM
|
||||
if (encodingToWrite === encoding.UTF16be || encodingToWrite === encoding.UTF16le || encodingToWrite === encoding.UTF8_with_bom) {
|
||||
@@ -599,7 +599,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return pfs.truncate(absolutePath, 0).then(() => {
|
||||
|
||||
// 5.) set contents (with r+ mode) and resolve
|
||||
return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { flag: 'r+' }).then(null, error => {
|
||||
return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { flag: 'r+' }).then(undefined, error => {
|
||||
if (this.environmentService.verbose) {
|
||||
console.error(`Truncate succeeded, but save failed (${error}), retrying after 100ms`);
|
||||
}
|
||||
@@ -622,7 +622,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
});
|
||||
});
|
||||
}).then(null, error => {
|
||||
}).then(undefined, error => {
|
||||
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
||||
return Promise.reject(new FileOperationError(
|
||||
nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)),
|
||||
@@ -635,7 +635,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): Thenable<IFileStat> {
|
||||
private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): Promise<IFileStat> {
|
||||
// Configure encoding related options as needed
|
||||
const writeFileOptions: extfs.IWriteFileOptions = options ? options : Object.create(null);
|
||||
if (addBOM || encodingToWrite !== encoding.UTF8) {
|
||||
@@ -645,7 +645,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
};
|
||||
}
|
||||
|
||||
let writeFilePromise: Thenable<void>;
|
||||
let writeFilePromise: Promise<void>;
|
||||
if (typeof value === 'string') {
|
||||
writeFilePromise = pfs.writeFile(absolutePath, value, writeFileOptions);
|
||||
} else {
|
||||
@@ -660,7 +660,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Thenable<IFileStat> {
|
||||
private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): Promise<IFileStat> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
// 1.) check file for writing
|
||||
@@ -678,7 +678,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const promptOptions = {
|
||||
name: this.environmentService.appNameLong.replace('-', ''),
|
||||
icns: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : void 0
|
||||
icns: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : undefined
|
||||
};
|
||||
|
||||
const sudoCommand: string[] = [`"${this.environmentService.cliPath}"`];
|
||||
@@ -691,7 +691,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
if (error || stderr) {
|
||||
reject(error || stderr);
|
||||
} else {
|
||||
resolve(void 0);
|
||||
resolve(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -705,7 +705,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
});
|
||||
});
|
||||
}).then(null, error => {
|
||||
}).then(undefined, error => {
|
||||
if (this.environmentService.verbose) {
|
||||
this.handleError(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`);
|
||||
}
|
||||
@@ -722,10 +722,10 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): Thenable<IFileStat> {
|
||||
createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): Promise<IFileStat> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
let checkFilePromise: Thenable<boolean>;
|
||||
let checkFilePromise: Promise<boolean>;
|
||||
if (options.overwrite) {
|
||||
checkFilePromise = Promise.resolve(false);
|
||||
} else {
|
||||
@@ -753,13 +753,13 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
readFolder(resource: uri): Thenable<string[]> {
|
||||
readFolder(resource: uri): Promise<string[]> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
return pfs.readdir(absolutePath);
|
||||
}
|
||||
|
||||
createFolder(resource: uri): Thenable<IFileStat> {
|
||||
createFolder(resource: uri): Promise<IFileStat> {
|
||||
|
||||
// 1.) Create folder
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
@@ -776,7 +776,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
private checkFileBeforeWriting(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): Thenable<boolean /* exists */> {
|
||||
private checkFileBeforeWriting(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): Promise<boolean /* exists */> {
|
||||
return pfs.exists(absolutePath).then(exists => {
|
||||
if (exists) {
|
||||
return pfs.stat(absolutePath).then(stat => {
|
||||
@@ -830,7 +830,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
});
|
||||
}
|
||||
|
||||
private readOnlyError<T>(options: IUpdateContentOptions): Thenable<T> {
|
||||
private readOnlyError<T>(options: IUpdateContentOptions): Promise<T> {
|
||||
return Promise.reject(new FileOperationError(
|
||||
nls.localize('fileReadOnlyError', "File is Read Only"),
|
||||
FileOperationResult.FILE_READ_ONLY,
|
||||
@@ -838,15 +838,15 @@ export class FileService extends Disposable implements IFileService {
|
||||
));
|
||||
}
|
||||
|
||||
moveFile(source: uri, target: uri, overwrite?: boolean): Thenable<IFileStat> {
|
||||
moveFile(source: uri, target: uri, overwrite?: boolean): Promise<IFileStat> {
|
||||
return this.moveOrCopyFile(source, target, false, overwrite);
|
||||
}
|
||||
|
||||
copyFile(source: uri, target: uri, overwrite?: boolean): Thenable<IFileStat> {
|
||||
copyFile(source: uri, target: uri, overwrite?: boolean): Promise<IFileStat> {
|
||||
return this.moveOrCopyFile(source, target, true, overwrite);
|
||||
}
|
||||
|
||||
private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): Thenable<IFileStat> {
|
||||
private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): Promise<IFileStat> {
|
||||
const sourcePath = this.toAbsolutePath(source);
|
||||
const targetPath = this.toAbsolutePath(target);
|
||||
|
||||
@@ -856,25 +856,28 @@ export class FileService extends Disposable implements IFileService {
|
||||
// 2.) resolve
|
||||
return this.resolve(target).then(result => {
|
||||
|
||||
// Events
|
||||
this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result));
|
||||
// Events (unless it was a no-op because paths are identical)
|
||||
if (sourcePath !== targetPath) {
|
||||
this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): Thenable<boolean /* exists */> {
|
||||
private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): Promise<void> {
|
||||
|
||||
// 1.) validate operation
|
||||
if (isParent(targetPath, sourcePath, !isLinux)) {
|
||||
return Promise.reject(new Error('Unable to move/copy when source path is parent of target path'));
|
||||
} else if (sourcePath === targetPath) {
|
||||
return Promise.resolve(); // no-op but not an error
|
||||
}
|
||||
|
||||
// 2.) check if target exists
|
||||
return pfs.exists(targetPath).then(exists => {
|
||||
const isCaseRename = sourcePath.toLowerCase() === targetPath.toLowerCase();
|
||||
const isSameFile = sourcePath === targetPath;
|
||||
|
||||
// Return early with conflict if target exists and we are not told to overwrite
|
||||
if (exists && !isCaseRename && !overwrite) {
|
||||
@@ -882,7 +885,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
}
|
||||
|
||||
// 3.) make sure target is deleted before we move/copy unless this is a case rename of the same file
|
||||
let deleteTargetPromise: Thenable<void> = Promise.resolve();
|
||||
let deleteTargetPromise: Promise<void> = Promise.resolve();
|
||||
if (exists && !isCaseRename) {
|
||||
if (isEqualOrParent(sourcePath, targetPath, !isLinux /* ignorecase */)) {
|
||||
return Promise.reject(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case!
|
||||
@@ -897,19 +900,17 @@ export class FileService extends Disposable implements IFileService {
|
||||
return pfs.mkdirp(paths.dirname(targetPath)).then(() => {
|
||||
|
||||
// 4.) copy/move
|
||||
if (isSameFile) {
|
||||
return null;
|
||||
} else if (keepCopy) {
|
||||
if (keepCopy) {
|
||||
return nfcall(extfs.copy, sourcePath, targetPath);
|
||||
} else {
|
||||
return nfcall(extfs.mv, sourcePath, targetPath);
|
||||
}
|
||||
}).then(() => exists);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
del(resource: uri, options?: { useTrash?: boolean, recursive?: boolean }): Thenable<void> {
|
||||
del(resource: uri, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
|
||||
if (options && options.useTrash) {
|
||||
return this.doMoveItemToTrash(resource);
|
||||
}
|
||||
@@ -917,7 +918,7 @@ export class FileService extends Disposable implements IFileService {
|
||||
return this.doDelete(resource, options && options.recursive);
|
||||
}
|
||||
|
||||
private doMoveItemToTrash(resource: uri): Thenable<void> {
|
||||
private doMoveItemToTrash(resource: uri): Promise<void> {
|
||||
const absolutePath = resource.fsPath;
|
||||
|
||||
const shell = (require('electron') as any as Electron.RendererInterface).shell; // workaround for being able to run tests out of VSCode debugger
|
||||
@@ -931,19 +932,19 @@ export class FileService extends Disposable implements IFileService {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private doDelete(resource: uri, recursive: boolean): Thenable<void> {
|
||||
private doDelete(resource: uri, recursive: boolean): Promise<void> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
let assertNonRecursiveDelete: Thenable<void>;
|
||||
let assertNonRecursiveDelete: Promise<void>;
|
||||
if (!recursive) {
|
||||
assertNonRecursiveDelete = pfs.stat(absolutePath).then(stat => {
|
||||
if (!stat.isDirectory()) {
|
||||
return Promise.resolve();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return pfs.readdir(absolutePath).then(children => {
|
||||
if (children.length === 0) {
|
||||
return Promise.resolve();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(nls.localize('deleteFailed', "Failed to delete non-empty folder '{0}'.", paths.basename(absolutePath))));
|
||||
@@ -977,15 +978,15 @@ export class FileService extends Disposable implements IFileService {
|
||||
return paths.normalize(resource.fsPath);
|
||||
}
|
||||
|
||||
private resolve(resource: uri, options: IResolveFileOptions = Object.create(null)): Thenable<IFileStat> {
|
||||
private resolve(resource: uri, options: IResolveFileOptions = Object.create(null)): Promise<IFileStat> {
|
||||
return this.toStatResolver(resource).then(model => model.resolve(options));
|
||||
}
|
||||
|
||||
private toStatResolver(resource: uri): Thenable<StatResolver> {
|
||||
private toStatResolver(resource: uri): Promise<StatResolver> {
|
||||
const absolutePath = this.toAbsolutePath(resource);
|
||||
|
||||
return pfs.statLink(absolutePath).then(({ isSymbolicLink, stat }) => {
|
||||
return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.environmentService.verbose ? err => this.handleError(err) : void 0);
|
||||
return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.environmentService.verbose ? err => this.handleError(err) : undefined);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1141,7 +1142,7 @@ export class StatResolver {
|
||||
this.etag = etag(size, mtime);
|
||||
}
|
||||
|
||||
resolve(options: IResolveFileOptions): Thenable<IFileStat> {
|
||||
resolve(options: IResolveFileOptions): Promise<IFileStat> {
|
||||
|
||||
// General Data
|
||||
const fileStat: IFileStat = {
|
||||
|
||||
@@ -36,7 +36,7 @@ class TypeOnlyStat implements IStat {
|
||||
size: number = 0;
|
||||
}
|
||||
|
||||
function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): Thenable<IFileStat> {
|
||||
function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse?: (tuple: [URI, IStat]) => boolean): Promise<IFileStat> {
|
||||
const [resource, stat] = tuple;
|
||||
const fileStat: IFileStat = {
|
||||
resource,
|
||||
@@ -70,7 +70,7 @@ function toIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], recurse
|
||||
return Promise.resolve(fileStat);
|
||||
}
|
||||
|
||||
export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], to: URI[]): Thenable<IFileStat> {
|
||||
export function toDeepIFileStat(provider: IFileSystemProvider, tuple: [URI, IStat], to: URI[]): Promise<IFileStat> {
|
||||
|
||||
const trie = TernarySearchTree.forPaths<true>();
|
||||
trie.set(tuple[0].toString(), true);
|
||||
@@ -90,8 +90,8 @@ class WorkspaceWatchLogic extends Disposable {
|
||||
|
||||
constructor(
|
||||
private _fileService: RemoteFileService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IWorkspaceContextService private _contextService: IWorkspaceContextService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -205,7 +205,7 @@ export class RemoteFileService extends FileService {
|
||||
};
|
||||
}
|
||||
|
||||
activateProvider(scheme: string): Thenable<void> {
|
||||
activateProvider(scheme: string): Promise<void> {
|
||||
return this._extensionService.activateByEvent('onFileSystem:' + scheme);
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ export class RemoteFileService extends FileService {
|
||||
});
|
||||
}
|
||||
|
||||
existsFile(resource: URI): Thenable<boolean> {
|
||||
existsFile(resource: URI): Promise<boolean> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.existsFile(resource);
|
||||
} else {
|
||||
@@ -277,7 +277,7 @@ export class RemoteFileService extends FileService {
|
||||
}
|
||||
}
|
||||
|
||||
resolveFile(resource: URI, options?: IResolveFileOptions): Thenable<IFileStat> {
|
||||
resolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.resolveFile(resource, options);
|
||||
} else {
|
||||
@@ -294,10 +294,10 @@ export class RemoteFileService extends FileService {
|
||||
}
|
||||
}
|
||||
|
||||
resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Thenable<IResolveFileResult[]> {
|
||||
resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise<IResolveFileResult[]> {
|
||||
|
||||
// soft-groupBy, keep order, don't rearrange/merge groups
|
||||
let groups: (typeof toResolve)[] = [];
|
||||
let groups: Array<typeof toResolve> = [];
|
||||
let group: typeof toResolve;
|
||||
for (const request of toResolve) {
|
||||
if (!group || group[0].resource.scheme !== request.resource.scheme) {
|
||||
@@ -307,7 +307,7 @@ export class RemoteFileService extends FileService {
|
||||
group.push(request);
|
||||
}
|
||||
|
||||
const promises: Thenable<IResolveFileResult[]>[] = [];
|
||||
const promises: Promise<IResolveFileResult[]>[] = [];
|
||||
for (const group of groups) {
|
||||
if (group[0].resource.scheme === Schemas.file) {
|
||||
promises.push(super.resolveFiles(group));
|
||||
@@ -336,7 +336,7 @@ export class RemoteFileService extends FileService {
|
||||
|
||||
// --- resolve
|
||||
|
||||
resolveContent(resource: URI, options?: IResolveContentOptions): Thenable<IContent> {
|
||||
resolveContent(resource: URI, options?: IResolveContentOptions): Promise<IContent> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.resolveContent(resource, options);
|
||||
} else {
|
||||
@@ -344,7 +344,7 @@ export class RemoteFileService extends FileService {
|
||||
}
|
||||
}
|
||||
|
||||
resolveStreamContent(resource: URI, options?: IResolveContentOptions): Thenable<IStreamContent> {
|
||||
resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise<IStreamContent> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.resolveStreamContent(resource, options);
|
||||
} else {
|
||||
@@ -438,7 +438,7 @@ export class RemoteFileService extends FileService {
|
||||
return provider;
|
||||
}
|
||||
|
||||
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Thenable<IFileStat> {
|
||||
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise<IFileStat> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.createFile(resource, content, options);
|
||||
} else {
|
||||
@@ -461,7 +461,7 @@ export class RemoteFileService extends FileService {
|
||||
}
|
||||
}
|
||||
|
||||
updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Thenable<IFileStat> {
|
||||
updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): Promise<IFileStat> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.updateContent(resource, value, options);
|
||||
} else {
|
||||
@@ -482,7 +482,7 @@ export class RemoteFileService extends FileService {
|
||||
return new Promise<IFileStat>((resolve, reject) => {
|
||||
readable.pipe(encoder).pipe(target);
|
||||
target.once('error', err => reject(err));
|
||||
target.once('finish', _ => resolve(void 0));
|
||||
target.once('finish', _ => resolve(undefined));
|
||||
}).then(_ => {
|
||||
return this.resolveFile(resource);
|
||||
});
|
||||
@@ -507,7 +507,7 @@ export class RemoteFileService extends FileService {
|
||||
|
||||
// --- delete
|
||||
|
||||
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Thenable<void> {
|
||||
del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.del(resource, options);
|
||||
} else {
|
||||
@@ -519,7 +519,7 @@ export class RemoteFileService extends FileService {
|
||||
}
|
||||
}
|
||||
|
||||
readFolder(resource: URI): Thenable<string[]> {
|
||||
readFolder(resource: URI): Promise<string[]> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.readFolder(resource);
|
||||
} else {
|
||||
@@ -529,7 +529,7 @@ export class RemoteFileService extends FileService {
|
||||
}
|
||||
}
|
||||
|
||||
createFolder(resource: URI): Thenable<IFileStat> {
|
||||
createFolder(resource: URI): Promise<IFileStat> {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
return super.createFolder(resource);
|
||||
} else {
|
||||
@@ -546,7 +546,7 @@ export class RemoteFileService extends FileService {
|
||||
}
|
||||
}
|
||||
|
||||
moveFile(source: URI, target: URI, overwrite?: boolean): Thenable<IFileStat> {
|
||||
moveFile(source: URI, target: URI, overwrite?: boolean): Promise<IFileStat> {
|
||||
if (source.scheme !== target.scheme) {
|
||||
return this._doMoveAcrossScheme(source, target);
|
||||
} else if (source.scheme === Schemas.file) {
|
||||
@@ -563,22 +563,24 @@ export class RemoteFileService extends FileService {
|
||||
: Promise.resolve(null);
|
||||
|
||||
return prepare.then(() => this._withProvider(source)).then(RemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
|
||||
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;
|
||||
return RemoteFileService._mkdirp(provider, resources.dirname(target)).then(() => {
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _doMoveAcrossScheme(source: URI, target: URI, overwrite?: boolean): Thenable<IFileStat> {
|
||||
private _doMoveAcrossScheme(source: URI, target: URI, overwrite?: boolean): Promise<IFileStat> {
|
||||
return this.copyFile(source, target, overwrite).then(() => {
|
||||
return this.del(source, { recursive: true });
|
||||
}).then(() => {
|
||||
@@ -589,7 +591,7 @@ export class RemoteFileService extends FileService {
|
||||
});
|
||||
}
|
||||
|
||||
copyFile(source: URI, target: URI, overwrite?: boolean): Thenable<IFileStat> {
|
||||
copyFile(source: URI, target: URI, overwrite?: boolean): Promise<IFileStat> {
|
||||
if (source.scheme === target.scheme && source.scheme === Schemas.file) {
|
||||
return super.copyFile(source, target, overwrite);
|
||||
}
|
||||
@@ -646,7 +648,7 @@ export class RemoteFileService extends FileService {
|
||||
});
|
||||
}
|
||||
|
||||
private _activeWatches = new Map<string, { unwatch: Thenable<IDisposable>, count: number }>();
|
||||
private _activeWatches = new Map<string, { unwatch: Promise<IDisposable>, count: number }>();
|
||||
|
||||
watchFileChanges(resource: URI, opts?: IWatchOptions): void {
|
||||
if (resource.scheme === Schemas.file) {
|
||||
|
||||
@@ -43,14 +43,14 @@ function createSimpleWritable(provider: IFileSystemProvider, resource: URI, opts
|
||||
function createWritable(provider: IFileSystemProvider, resource: URI, opts: FileWriteOptions): Writable {
|
||||
return new class extends Writable {
|
||||
_fd: number;
|
||||
_pos: number;
|
||||
_pos: number = 0;
|
||||
constructor(opts?) {
|
||||
super(opts);
|
||||
}
|
||||
async _write(chunk: Buffer, encoding, callback: Function) {
|
||||
try {
|
||||
if (typeof this._fd !== 'number') {
|
||||
this._fd = await provider.open(resource);
|
||||
this._fd = await provider.open(resource, { create: true });
|
||||
}
|
||||
let bytesWritten = await provider.write(this._fd, this._pos, chunk, 0, chunk.length);
|
||||
this._pos += bytesWritten;
|
||||
@@ -59,12 +59,12 @@ function createWritable(provider: IFileSystemProvider, resource: URI, opts: File
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
end() {
|
||||
provider.close(this._fd).then(_ => {
|
||||
super.end();
|
||||
}, err => {
|
||||
this.emit('error', err);
|
||||
});
|
||||
_final(callback: (err?: any) => any) {
|
||||
if (typeof this._fd !== 'number') {
|
||||
provider.open(resource, { create: true }).then(fd => provider.close(fd)).finally(callback);
|
||||
} else {
|
||||
provider.close(this._fd).finally(callback);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -84,10 +84,7 @@ function createReadable(provider: IFileSystemProvider, resource: URI, position:
|
||||
_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;
|
||||
@@ -95,29 +92,28 @@ function createReadable(provider: IFileSystemProvider, resource: URI, position:
|
||||
this._reading = true;
|
||||
try {
|
||||
if (typeof this._fd !== 'number') {
|
||||
this._fd = await provider.open(resource);
|
||||
this._fd = await provider.open(resource, { create: false });
|
||||
}
|
||||
let buffer = Buffer.allocUnsafe(64 * 1024);
|
||||
while (this._reading) {
|
||||
let buffer = Buffer.allocUnsafe(size);
|
||||
let bytesRead = await provider.read(this._fd, this._pos, buffer, 0, buffer.length);
|
||||
if (bytesRead === 0) {
|
||||
await provider.close(this._fd);
|
||||
this._reading = false;
|
||||
this.push(null);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this._reading = this.push(buffer.slice(0, bytesRead));
|
||||
this._pos += bytesRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
//
|
||||
this.emit('error', err);
|
||||
}
|
||||
}
|
||||
async _final() {
|
||||
_destroy(_err: any, callback: (err?: any) => any) {
|
||||
if (typeof this._fd === 'number') {
|
||||
await provider.close(this._fd);
|
||||
provider.close(this._fd).then(callback, callback);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -125,7 +121,7 @@ function createReadable(provider: IFileSystemProvider, resource: URI, position:
|
||||
|
||||
function createSimpleReadable(provider: IFileSystemProvider, resource: URI, position: number): Readable {
|
||||
return new class extends Readable {
|
||||
_readOperation: Thenable<any>;
|
||||
_readOperation: Promise<any>;
|
||||
_read(size?: number): void {
|
||||
if (this._readOperation) {
|
||||
return;
|
||||
|
||||
@@ -30,8 +30,7 @@ export function normalize(changes: IRawFileChange[]): IRawFileChange[] {
|
||||
|
||||
// Build deltas
|
||||
let normalizer = new EventNormalizer();
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
let event = changes[i];
|
||||
for (const event of changes) {
|
||||
normalizer.processEvent(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ interface IWatcherObjet {
|
||||
}
|
||||
|
||||
interface IPathWatcher {
|
||||
ready: Thenable<IWatcherObjet>;
|
||||
ready: Promise<IWatcherObjet>;
|
||||
watcher?: IWatcherObjet;
|
||||
ignored: glob.ParsedPattern[];
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
|
||||
private _watch(request: IWatcherRequest): void {
|
||||
let undeliveredFileEvents: watcher.IRawFileChange[] = [];
|
||||
const fileEventDelayer = new ThrottledDelayer(NsfwWatcherService.FS_EVENT_DELAY);
|
||||
const fileEventDelayer = new ThrottledDelayer<void>(NsfwWatcherService.FS_EVENT_DELAY);
|
||||
|
||||
let readyPromiseResolve: (watcher: IWatcherObjet) => void;
|
||||
this._pathWatchers[request.basePath] = {
|
||||
@@ -99,12 +99,10 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
}
|
||||
|
||||
nsfw(request.basePath, events => {
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const e = events[i];
|
||||
|
||||
for (const e of events) {
|
||||
// Logging
|
||||
if (this._verboseLogging) {
|
||||
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile) + ' -> ' + e.newFile : path.join(e.directory, e.file);
|
||||
const logPath = e.action === nsfw.actions.RENAMED ? path.join(e.directory, e.oldFile || '') + ' -> ' + e.newFile : path.join(e.directory, e.file || '');
|
||||
console.log(`${e.action === nsfw.actions.CREATED ? '[CREATED]' : e.action === nsfw.actions.DELETED ? '[DELETED]' : e.action === nsfw.actions.MODIFIED ? '[CHANGED]' : '[RENAMED]'} ${logPath}`);
|
||||
}
|
||||
|
||||
@@ -112,20 +110,20 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
let absolutePath: string;
|
||||
if (e.action === nsfw.actions.RENAMED) {
|
||||
// Rename fires when a file's name changes within a single directory
|
||||
absolutePath = path.join(e.directory, e.oldFile);
|
||||
absolutePath = path.join(e.directory, e.oldFile || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
|
||||
undeliveredFileEvents.push({ type: FileChangeType.DELETED, path: absolutePath });
|
||||
} else if (this._verboseLogging) {
|
||||
console.log(' >> ignored', absolutePath);
|
||||
}
|
||||
absolutePath = path.join(e.directory, e.newFile);
|
||||
absolutePath = path.join(e.directory, e.newFile || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
|
||||
undeliveredFileEvents.push({ type: FileChangeType.ADDED, path: absolutePath });
|
||||
} else if (this._verboseLogging) {
|
||||
console.log(' >> ignored', absolutePath);
|
||||
}
|
||||
} else {
|
||||
absolutePath = path.join(e.directory, e.file);
|
||||
absolutePath = path.join(e.directory, e.file || '');
|
||||
if (!this._isPathIgnored(absolutePath, this._pathWatchers[request.basePath].ignored)) {
|
||||
undeliveredFileEvents.push({
|
||||
type: nsfwActionToRawChangeType[e.action],
|
||||
@@ -166,7 +164,7 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
}).then(watcher => {
|
||||
this._pathWatchers[request.basePath].watcher = watcher;
|
||||
@@ -176,8 +174,8 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
});
|
||||
}
|
||||
|
||||
public setRoots(roots: IWatcherRequest[]): Thenable<void> {
|
||||
const promises: Thenable<void>[] = [];
|
||||
public setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
const normalizedRoots = this._normalizeRoots(roots);
|
||||
|
||||
// Gather roots that are not currently being watched
|
||||
@@ -211,15 +209,15 @@ export class NsfwWatcherService implements IWatcherService {
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => void 0);
|
||||
return Promise.all(promises).then(() => undefined);
|
||||
}
|
||||
|
||||
public setVerboseLogging(enabled: boolean): Thenable<void> {
|
||||
public setVerboseLogging(enabled: boolean): Promise<void> {
|
||||
this._verboseLogging = enabled;
|
||||
return Promise.resolve(null);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public stop(): Thenable<void> {
|
||||
public stop(): Promise<void> {
|
||||
for (let path in this._pathWatchers) {
|
||||
let watcher = this._pathWatchers[path];
|
||||
watcher.ready.then(watcher => watcher.stop());
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface IWatchError {
|
||||
|
||||
export interface IWatcherService {
|
||||
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError>;
|
||||
setRoots(roots: IWatcherRequest[]): Thenable<void>;
|
||||
setVerboseLogging(enabled: boolean): Thenable<void>;
|
||||
stop(): Thenable<void>;
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void>;
|
||||
setVerboseLogging(enabled: boolean): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export class WatcherChannel implements IServerChannel {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(_, command: string, arg?: any): Thenable<any> {
|
||||
call(_, command: string, arg?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'setRoots': return this.service.setRoots(arg);
|
||||
case 'setVerboseLogging': return this.service.setVerboseLogging(arg);
|
||||
@@ -39,15 +39,15 @@ export class WatcherChannelClient implements IWatcherService {
|
||||
return this.channel.listen('watch', options);
|
||||
}
|
||||
|
||||
setVerboseLogging(enable: boolean): Thenable<void> {
|
||||
setVerboseLogging(enable: boolean): Promise<void> {
|
||||
return this.channel.call('setVerboseLogging', enable);
|
||||
}
|
||||
|
||||
setRoots(roots: IWatcherRequest[]): Thenable<void> {
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
return this.channel.call('setRoots', roots);
|
||||
}
|
||||
|
||||
stop(): Thenable<void> {
|
||||
stop(): Promise<void> {
|
||||
return this.channel.call('stop');
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { filterEvent } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IWatchError } from 'vs/workbench/services/files/node/watcher/nsfw/watcher';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
@@ -69,12 +69,12 @@ export class FileWatcher {
|
||||
this.service = new WatcherChannelClient(channel);
|
||||
|
||||
const options = { verboseLogging: this.verboseLogging };
|
||||
const onWatchEvent = filterEvent(this.service.watch(options), () => !this.isDisposed);
|
||||
const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed);
|
||||
|
||||
const onError = filterEvent<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
|
||||
const onError = Event.filter<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
|
||||
onError(err => this.errorLogger(err.message), null, this.toDispose);
|
||||
|
||||
const onFileChanges = filterEvent<any, IRawFileChange[]>(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0);
|
||||
const onFileChanges = Event.filter<any, IRawFileChange[]>(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0);
|
||||
onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose);
|
||||
|
||||
// Start watching
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as paths from 'vs/base/common/paths';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { normalizeNFC } from 'vs/base/common/normalization';
|
||||
import { realcaseSync } from 'vs/base/node/extfs';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
@@ -58,13 +57,13 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
return this.onWatchEvent;
|
||||
}
|
||||
|
||||
public setVerboseLogging(enabled: boolean): Thenable<void> {
|
||||
public setVerboseLogging(enabled: boolean): Promise<void> {
|
||||
this._verboseLogging = enabled;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public setRoots(requests: IWatcherRequest[]): Thenable<void> {
|
||||
public setRoots(requests: IWatcherRequest[]): Promise<void> {
|
||||
const watchers = Object.create(null);
|
||||
const newRequests: string[] = [];
|
||||
|
||||
@@ -220,7 +219,7 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
this.spamCheckStartTime = now;
|
||||
} else if (!this.spamWarningLogged && this.spamCheckStartTime + ChokidarWatcherService.EVENT_SPAM_WARNING_THRESHOLD < now) {
|
||||
this.spamWarningLogged = true;
|
||||
console.warn(strings.format('Watcher is busy catching up with {0} file changes in 60 seconds. Latest changed path is "{1}"', undeliveredFileEvents.length, event.path));
|
||||
console.warn(`Watcher is busy catching up with ${undeliveredFileEvents.length} file changes in 60 seconds. Latest changed path is "${event.path}"`);
|
||||
}
|
||||
|
||||
// Add to buffer
|
||||
@@ -270,7 +269,7 @@ export class ChokidarWatcherService implements IWatcherService {
|
||||
return watcher;
|
||||
}
|
||||
|
||||
public stop(): Thenable<void> {
|
||||
public stop(): Promise<void> {
|
||||
for (let path in this._watchers) {
|
||||
let watcher = this._watchers[path];
|
||||
watcher.stop();
|
||||
|
||||
@@ -16,7 +16,7 @@ import { Delayer } from 'vs/base/common/async';
|
||||
import { IRawFileChange } from 'vs/workbench/services/files/node/watcher/common';
|
||||
import { FileChangeType } from 'vs/platform/files/common/files';
|
||||
|
||||
function newRequest(basePath: string, ignored = []): IWatcherRequest {
|
||||
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
|
||||
return { basePath, ignored };
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ suite.skip('Chockidar watching', () => {
|
||||
|
||||
const service = new ChokidarWatcherService();
|
||||
const result: IRawFileChange[] = [];
|
||||
let error = null;
|
||||
let error: string | null = null;
|
||||
|
||||
suiteSetup(async () => {
|
||||
await pfs.mkdirp(testDir);
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface IWatchError {
|
||||
|
||||
export interface IWatcherService {
|
||||
watch(options: IWatcherOptions): Event<IRawFileChange[] | IWatchError>;
|
||||
setRoots(roots: IWatcherRequest[]): Thenable<void>;
|
||||
setVerboseLogging(enabled: boolean): Thenable<void>;
|
||||
stop(): Thenable<void>;
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void>;
|
||||
setVerboseLogging(enabled: boolean): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export class WatcherChannel implements IServerChannel {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(_, command: string, arg?: any): Thenable<any> {
|
||||
call(_, command: string, arg?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'setRoots': return this.service.setRoots(arg);
|
||||
case 'setVerboseLogging': return this.service.setVerboseLogging(arg);
|
||||
@@ -39,15 +39,15 @@ export class WatcherChannelClient implements IWatcherService {
|
||||
return this.channel.listen('watch', options);
|
||||
}
|
||||
|
||||
setVerboseLogging(enable: boolean): Thenable<void> {
|
||||
setVerboseLogging(enable: boolean): Promise<void> {
|
||||
return this.channel.call('setVerboseLogging', enable);
|
||||
}
|
||||
|
||||
setRoots(roots: IWatcherRequest[]): Thenable<void> {
|
||||
setRoots(roots: IWatcherRequest[]): Promise<void> {
|
||||
return this.channel.call('setRoots', roots);
|
||||
}
|
||||
|
||||
stop(): Thenable<void> {
|
||||
stop(): Promise<void> {
|
||||
return this.channel.call('stop');
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { filterEvent } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IWatchError } from 'vs/workbench/services/files/node/watcher/unix/watcher';
|
||||
import { getPathFromAmdModule } from 'vs/base/common/amd';
|
||||
|
||||
@@ -71,12 +71,12 @@ export class FileWatcher {
|
||||
this.service = new WatcherChannelClient(channel);
|
||||
|
||||
const options = { verboseLogging: this.verboseLogging };
|
||||
const onWatchEvent = filterEvent(this.service.watch(options), () => !this.isDisposed);
|
||||
const onWatchEvent = Event.filter(this.service.watch(options), () => !this.isDisposed);
|
||||
|
||||
const onError = filterEvent<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
|
||||
const onError = Event.filter<any, IWatchError>(onWatchEvent, (e): e is IWatchError => typeof e.message === 'string');
|
||||
onError(err => this.errorLogger(err.message), null, this.toDispose);
|
||||
|
||||
const onFileChanges = filterEvent<any, IRawFileChange[]>(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0);
|
||||
const onFileChanges = Event.filter<any, IRawFileChange[]>(onWatchEvent, (e): e is IRawFileChange[] => Array.isArray(e) && e.length > 0);
|
||||
onFileChanges(e => this.onFileChanges(toFileChangesEvent(e)), null, this.toDispose);
|
||||
|
||||
// Start watching
|
||||
|
||||
@@ -58,7 +58,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.CREATE);
|
||||
assert.equal(event.target.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -69,7 +69,7 @@ suite('FileService', () => {
|
||||
|
||||
fs.writeFileSync(resource.fsPath, ''); // create file
|
||||
|
||||
return service.createFile(resource, contents).then(null, error => {
|
||||
return service.createFile(resource, contents).then(undefined, error => {
|
||||
assert.ok(error);
|
||||
});
|
||||
});
|
||||
@@ -93,7 +93,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.CREATE);
|
||||
assert.equal(event.target.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -114,8 +114,8 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.CREATE);
|
||||
assert.equal(event.target.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.target.isDirectory, true);
|
||||
assert.equal(event.target!.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.target!.isDirectory, true);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -139,8 +139,8 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.CREATE);
|
||||
assert.equal(event.target.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.target.isDirectory, true);
|
||||
assert.equal(event.target!.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.target!.isDirectory, true);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -161,7 +161,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.MOVE);
|
||||
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, renamed.resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -185,7 +185,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.MOVE);
|
||||
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, renamed.resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -206,7 +206,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.MOVE);
|
||||
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, renamed.resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -230,7 +230,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.MOVE);
|
||||
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, renamed.resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -250,7 +250,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.MOVE);
|
||||
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, renamed.resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -271,7 +271,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.MOVE);
|
||||
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, renamed.resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -284,7 +284,7 @@ suite('FileService', () => {
|
||||
});
|
||||
|
||||
return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => {
|
||||
return service.moveFile(uri.file(testDir), uri.file(path.join(testDir, 'binary.txt'))).then(null, (e: Error) => {
|
||||
return service.moveFile(uri.file(testDir), uri.file(path.join(testDir, 'binary.txt'))).then(undefined, (e: Error) => {
|
||||
assert.ok(e);
|
||||
|
||||
assert.ok(!event);
|
||||
@@ -300,7 +300,7 @@ suite('FileService', () => {
|
||||
});
|
||||
|
||||
return service.resolveFile(uri.file(path.join(testDir, 'index.html'))).then(source => {
|
||||
return service.moveFile(source.resource, uri.file(path.join(testDir, 'binary.txt'))).then(null, (e: FileOperationError) => {
|
||||
return service.moveFile(source.resource, uri.file(path.join(testDir, 'binary.txt'))).then(undefined, (e: FileOperationError) => {
|
||||
assert.equal(e.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT);
|
||||
|
||||
assert.ok(!event);
|
||||
@@ -324,7 +324,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.MOVE);
|
||||
assert.equal(event.target.resource.fsPath, renamed.resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, renamed.resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -357,7 +357,7 @@ suite('FileService', () => {
|
||||
assert.ok(moveEvent);
|
||||
|
||||
assert.equal(moveEvent.resource.fsPath, resource.fsPath);
|
||||
assert.equal(moveEvent.target.resource.fsPath, moved.resource.fsPath);
|
||||
assert.equal(moveEvent!.target!.resource.fsPath, moved.resource.fsPath);
|
||||
|
||||
assert.equal(deleteEvent.resource.fsPath, folderResource.fsPath);
|
||||
|
||||
@@ -382,7 +382,7 @@ suite('FileService', () => {
|
||||
assert.ok(event);
|
||||
assert.equal(event.resource.fsPath, source.resource.fsPath);
|
||||
assert.equal(event.operation, FileOperation.COPY);
|
||||
assert.equal(event.target.resource.fsPath, copied.resource.fsPath);
|
||||
assert.equal(event.target!.resource.fsPath, copied.resource.fsPath);
|
||||
toDispose.dispose();
|
||||
});
|
||||
});
|
||||
@@ -415,7 +415,7 @@ suite('FileService', () => {
|
||||
assert.ok(copyEvent);
|
||||
|
||||
assert.equal(copyEvent.resource.fsPath, resource.fsPath);
|
||||
assert.equal(copyEvent.target.resource.fsPath, copied.resource.fsPath);
|
||||
assert.equal(copyEvent.target!.resource.fsPath, copied.resource.fsPath);
|
||||
|
||||
assert.equal(deleteEvent.resource.fsPath, folderResource.fsPath);
|
||||
|
||||
@@ -505,10 +505,10 @@ suite('FileService', () => {
|
||||
|
||||
test('resolveFile', () => {
|
||||
return service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))] }).then(r => {
|
||||
assert.equal(r.children.length, 8);
|
||||
assert.equal(r.children!.length, 8);
|
||||
|
||||
const deep = utils.getByName(r, 'deep');
|
||||
assert.equal(deep.children.length, 4);
|
||||
const deep = utils.getByName(r, 'deep')!;
|
||||
assert.equal(deep.children!.length, 4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -519,13 +519,13 @@ suite('FileService', () => {
|
||||
]).then(res => {
|
||||
const r1 = res[0].stat;
|
||||
|
||||
assert.equal(r1.children.length, 8);
|
||||
assert.equal(r1.children!.length, 8);
|
||||
|
||||
const deep = utils.getByName(r1, 'deep');
|
||||
assert.equal(deep.children.length, 4);
|
||||
const deep = utils.getByName(r1, 'deep')!;
|
||||
assert.equal(deep.children!.length, 4);
|
||||
|
||||
const r2 = res[1].stat;
|
||||
assert.equal(r2.children.length, 4);
|
||||
assert.equal(r2.children!.length, 4);
|
||||
assert.equal(r2.name, 'deep');
|
||||
});
|
||||
});
|
||||
@@ -710,7 +710,7 @@ suite('FileService', () => {
|
||||
test('resolveContent - FILE_IS_BINARY', function () {
|
||||
const resource = uri.file(path.join(testDir, 'binary.txt'));
|
||||
|
||||
return service.resolveContent(resource, { acceptTextOnly: true }).then(null, (e: FileOperationError) => {
|
||||
return service.resolveContent(resource, { acceptTextOnly: true }).then(undefined, (e: FileOperationError) => {
|
||||
assert.equal(e.fileOperationResult, FileOperationResult.FILE_IS_BINARY);
|
||||
|
||||
return service.resolveContent(uri.file(path.join(testDir, 'small.txt')), { acceptTextOnly: true }).then(r => {
|
||||
@@ -722,7 +722,7 @@ suite('FileService', () => {
|
||||
test('resolveContent - FILE_IS_DIRECTORY', function () {
|
||||
const resource = uri.file(path.join(testDir, 'deep'));
|
||||
|
||||
return service.resolveContent(resource).then(null, (e: FileOperationError) => {
|
||||
return service.resolveContent(resource).then(undefined, (e: FileOperationError) => {
|
||||
assert.equal(e.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY);
|
||||
});
|
||||
});
|
||||
@@ -730,7 +730,7 @@ suite('FileService', () => {
|
||||
test('resolveContent - FILE_NOT_FOUND', function () {
|
||||
const resource = uri.file(path.join(testDir, '404.html'));
|
||||
|
||||
return service.resolveContent(resource).then(null, (e: FileOperationError) => {
|
||||
return service.resolveContent(resource).then(undefined, (e: FileOperationError) => {
|
||||
assert.equal(e.fileOperationResult, FileOperationResult.FILE_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
@@ -739,7 +739,7 @@ suite('FileService', () => {
|
||||
const resource = uri.file(path.join(testDir, 'index.html'));
|
||||
|
||||
return service.resolveContent(resource).then(c => {
|
||||
return service.resolveContent(resource, { etag: c.etag }).then(null, (e: FileOperationError) => {
|
||||
return service.resolveContent(resource, { etag: c.etag }).then(undefined, (e: FileOperationError) => {
|
||||
assert.equal(e.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE);
|
||||
});
|
||||
});
|
||||
@@ -751,7 +751,7 @@ suite('FileService', () => {
|
||||
return service.resolveContent(resource).then(c => {
|
||||
fs.writeFileSync(resource.fsPath, 'Updates Incoming!');
|
||||
|
||||
return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(null, (e: FileOperationError) => {
|
||||
return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(undefined, (e: FileOperationError) => {
|
||||
assert.equal(e.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,8 +8,8 @@ var Workforce;
|
||||
return Company;
|
||||
})();
|
||||
(function (property, Workforce, IEmployee) {
|
||||
if (property === void 0) { property = employees; }
|
||||
if (IEmployee === void 0) { IEmployee = []; }
|
||||
if (property === undefined) { property = employees; }
|
||||
if (IEmployee === undefined) { IEmployee = []; }
|
||||
property;
|
||||
calculateMonthlyExpenses();
|
||||
{
|
||||
|
||||
@@ -7,9 +7,9 @@ var Conway;
|
||||
return Cell;
|
||||
})();
|
||||
(function (property, number, property, number, property, boolean) {
|
||||
if (property === void 0) { property = row; }
|
||||
if (property === void 0) { property = col; }
|
||||
if (property === void 0) { property = live; }
|
||||
if (property === undefined) { property = row; }
|
||||
if (property === undefined) { property = col; }
|
||||
if (property === undefined) { property = live; }
|
||||
});
|
||||
var GameOfLife = (function () {
|
||||
function GameOfLife() {
|
||||
|
||||
@@ -7,10 +7,10 @@ var M;
|
||||
return C;
|
||||
})();
|
||||
(function (x, property, number) {
|
||||
if (property === void 0) { property = w; }
|
||||
if (property === undefined) { property = w; }
|
||||
var local = 1;
|
||||
// unresolved symbol because x is local
|
||||
//self.x++;
|
||||
//self.x++;
|
||||
self.w--; // ok because w is a property
|
||||
property;
|
||||
f = function (y) {
|
||||
|
||||
@@ -8,8 +8,8 @@ var Workforce;
|
||||
return Company;
|
||||
})();
|
||||
(function (property, Workforce, IEmployee) {
|
||||
if (property === void 0) { property = employees; }
|
||||
if (IEmployee === void 0) { IEmployee = []; }
|
||||
if (property === undefined) { property = employees; }
|
||||
if (IEmployee === undefined) { IEmployee = []; }
|
||||
property;
|
||||
calculateMonthlyExpenses();
|
||||
{
|
||||
|
||||
@@ -7,9 +7,9 @@ var Conway;
|
||||
return Cell;
|
||||
})();
|
||||
(function (property, number, property, number, property, boolean) {
|
||||
if (property === void 0) { property = row; }
|
||||
if (property === void 0) { property = col; }
|
||||
if (property === void 0) { property = live; }
|
||||
if (property === undefined) { property = row; }
|
||||
if (property === undefined) { property = col; }
|
||||
if (property === undefined) { property = live; }
|
||||
});
|
||||
var GameOfLife = (function () {
|
||||
function GameOfLife() {
|
||||
|
||||
@@ -7,10 +7,10 @@ var M;
|
||||
return C;
|
||||
})();
|
||||
(function (x, property, number) {
|
||||
if (property === void 0) { property = w; }
|
||||
if (property === undefined) { property = w; }
|
||||
var local = 1;
|
||||
// unresolved symbol because x is local
|
||||
//self.x++;
|
||||
//self.x++;
|
||||
self.w--; // ok because w is a property
|
||||
property;
|
||||
f = function (y) {
|
||||
|
||||
@@ -8,8 +8,8 @@ var Workforce;
|
||||
return Company;
|
||||
})();
|
||||
(function (property, Workforce, IEmployee) {
|
||||
if (property === void 0) { property = employees; }
|
||||
if (IEmployee === void 0) { IEmployee = []; }
|
||||
if (property === undefined) { property = employees; }
|
||||
if (IEmployee === undefined) { IEmployee = []; }
|
||||
property;
|
||||
calculateMonthlyExpenses();
|
||||
{
|
||||
|
||||
@@ -7,9 +7,9 @@ var Conway;
|
||||
return Cell;
|
||||
})();
|
||||
(function (property, number, property, number, property, boolean) {
|
||||
if (property === void 0) { property = row; }
|
||||
if (property === void 0) { property = col; }
|
||||
if (property === void 0) { property = live; }
|
||||
if (property === undefined) { property = row; }
|
||||
if (property === undefined) { property = col; }
|
||||
if (property === undefined) { property = live; }
|
||||
});
|
||||
var GameOfLife = (function () {
|
||||
function GameOfLife() {
|
||||
|
||||
@@ -7,10 +7,10 @@ var M;
|
||||
return C;
|
||||
})();
|
||||
(function (x, property, number) {
|
||||
if (property === void 0) { property = w; }
|
||||
if (property === undefined) { property = w; }
|
||||
var local = 1;
|
||||
// unresolved symbol because x is local
|
||||
//self.x++;
|
||||
//self.x++;
|
||||
self.w--; // ok because w is a property
|
||||
property;
|
||||
f = function (y) {
|
||||
|
||||
@@ -18,7 +18,7 @@ function create(relativePath: string): StatResolver {
|
||||
let absolutePath = relativePath ? path.join(basePath, relativePath) : basePath;
|
||||
let fsStat = fs.statSync(absolutePath);
|
||||
|
||||
return new StatResolver(uri.file(absolutePath), fsStat.isSymbolicLink(), fsStat.isDirectory(), fsStat.mtime.getTime(), fsStat.size, void 0);
|
||||
return new StatResolver(uri.file(absolutePath), fsStat.isSymbolicLink(), fsStat.isDirectory(), fsStat.mtime.getTime(), fsStat.size, undefined);
|
||||
}
|
||||
|
||||
function toResource(relativePath: string): uri {
|
||||
|
||||
@@ -10,9 +10,9 @@ export function getByName(root: IFileStat, name: string): IFileStat | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < root.children.length; i++) {
|
||||
if (root.children[i].name === name) {
|
||||
return root.children[i];
|
||||
for (const child of root.children) {
|
||||
if (child.name === name) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Event } from 'vs/base/common/event';
|
||||
import { createDecorator, ServiceIdentifier, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, CloseDirection } from 'vs/workbench/common/editor';
|
||||
import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export const IEditorGroupsService = createDecorator<IEditorGroupsService>('editorGroupsService');
|
||||
@@ -377,18 +376,20 @@ export interface IEditorGroup {
|
||||
/**
|
||||
* Open an editor in this group.
|
||||
*
|
||||
* @returns a promise that is resolved when the active editor (if any)
|
||||
* has finished loading
|
||||
* @returns a promise that resolves around an IEditor instance unless
|
||||
* the call failed, or the editor was not opened as active editor.
|
||||
*/
|
||||
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): TPromise<void>;
|
||||
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): Promise<IEditor>;
|
||||
|
||||
/**
|
||||
* Opens editors in this group.
|
||||
*
|
||||
* @returns a promise that is resolved when the active editor (if any)
|
||||
* has finished loading
|
||||
* @returns a promise that resolves around an IEditor instance unless
|
||||
* the call failed, or the editor was not opened as active editor. Since
|
||||
* a group can only ever have one active editor, even if many editors are
|
||||
* opened, the result will only be one editor.
|
||||
*/
|
||||
openEditors(editors: IEditorInputWithOptions[]): TPromise<void>;
|
||||
openEditors(editors: IEditorInputWithOptions[]): Promise<IEditor>;
|
||||
|
||||
/**
|
||||
* Find out if the provided editor is opened in the group.
|
||||
@@ -428,7 +429,7 @@ export interface IEditorGroup {
|
||||
*
|
||||
* @returns a promise when the editor is closed.
|
||||
*/
|
||||
closeEditor(editor?: IEditorInput): TPromise<void>;
|
||||
closeEditor(editor?: IEditorInput): Promise<void>;
|
||||
|
||||
/**
|
||||
* Closes specific editors in this group. This may trigger a confirmation dialog if
|
||||
@@ -436,7 +437,7 @@ export interface IEditorGroup {
|
||||
*
|
||||
* @returns a promise when all editors are closed.
|
||||
*/
|
||||
closeEditors(editors: IEditorInput[] | ICloseEditorsFilter): TPromise<void>;
|
||||
closeEditors(editors: IEditorInput[] | ICloseEditorsFilter): Promise<void>;
|
||||
|
||||
/**
|
||||
* Closes all editors from the group. This may trigger a confirmation dialog if
|
||||
@@ -444,7 +445,7 @@ export interface IEditorGroup {
|
||||
*
|
||||
* @returns a promise when all editors are closed.
|
||||
*/
|
||||
closeAllEditors(): TPromise<void>;
|
||||
closeAllEditors(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Replaces editors in this group with the provided replacement.
|
||||
@@ -454,7 +455,7 @@ export interface IEditorGroup {
|
||||
* @returns a promise that is resolved when the replaced active
|
||||
* editor (if any) has finished loading.
|
||||
*/
|
||||
replaceEditors(editors: IEditorReplacement[]): TPromise<void>;
|
||||
replaceEditors(editors: IEditorReplacement[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Set an editor to be pinned. A pinned editor is not replaced
|
||||
|
||||
@@ -63,7 +63,7 @@ export interface IHistoryService {
|
||||
/**
|
||||
* Get the entire history of opened editors.
|
||||
*/
|
||||
getHistory(): (IEditorInput | IResourceInput)[];
|
||||
getHistory(): Array<IEditorInput | IResourceInput>;
|
||||
|
||||
/**
|
||||
* Looking at the editor history, returns the workspace root of the last file that was
|
||||
@@ -71,12 +71,12 @@ export interface IHistoryService {
|
||||
*
|
||||
* @param schemeFilter filter to restrict roots by scheme.
|
||||
*/
|
||||
getLastActiveWorkspaceRoot(schemeFilter?: string): URI;
|
||||
getLastActiveWorkspaceRoot(schemeFilter?: string): URI | undefined;
|
||||
|
||||
/**
|
||||
* Looking at the editor history, returns the resource of the last file that was opened.
|
||||
*
|
||||
* @param schemeFilter filter to restrict roots by scheme.
|
||||
*/
|
||||
getLastActiveFile(schemeFilter: string): URI;
|
||||
getLastActiveFile(schemeFilter: string): URI | undefined;
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
|
||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { once, debounceEvent } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
import { IWindowsService } from 'vs/platform/windows/common/windows';
|
||||
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { getExcludes, ISearchConfiguration } from 'vs/platform/search/common/search';
|
||||
import { IExpression } from 'vs/base/common/glob';
|
||||
import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
@@ -29,6 +29,7 @@ import { ResourceGlobMatcher } from 'vs/workbench/electron-browser/resources';
|
||||
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
|
||||
import { IPartService } from 'vs/workbench/services/part/common/partService';
|
||||
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
|
||||
/**
|
||||
* Stores the selection & view state of an editor and allows to compare it to other selection states.
|
||||
@@ -37,20 +38,20 @@ export class TextEditorState {
|
||||
|
||||
private static readonly EDITOR_SELECTION_THRESHOLD = 10; // number of lines to move in editor to justify for new state
|
||||
|
||||
private textEditorSelection: ITextEditorSelection;
|
||||
private textEditorSelection?: ITextEditorSelection;
|
||||
|
||||
constructor(private _editorInput: IEditorInput, private _selection: Selection) {
|
||||
constructor(private _editorInput: IEditorInput, private _selection: Selection | null) {
|
||||
this.textEditorSelection = Selection.isISelection(_selection) ? {
|
||||
startLineNumber: _selection.startLineNumber,
|
||||
startColumn: _selection.startColumn
|
||||
} : void 0;
|
||||
} : undefined;
|
||||
}
|
||||
|
||||
get editorInput(): IEditorInput {
|
||||
return this._editorInput;
|
||||
}
|
||||
|
||||
get selection(): ITextEditorSelection {
|
||||
get selection(): ITextEditorSelection | undefined {
|
||||
return this.textEditorSelection;
|
||||
}
|
||||
|
||||
@@ -103,7 +104,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
private static readonly MAX_RECENTLY_CLOSED_EDITORS = 20;
|
||||
|
||||
private activeEditorListeners: IDisposable[];
|
||||
private lastActiveEditor: IEditorIdentifier;
|
||||
private lastActiveEditor?: IEditorIdentifier;
|
||||
|
||||
private editorHistoryListeners: Map<EditorInput, IDisposable[]> = new Map();
|
||||
private editorStackListeners: Map<EditorInput, IDisposable[]> = new Map();
|
||||
@@ -112,11 +113,11 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
private index: number;
|
||||
private lastIndex: number;
|
||||
private navigatingInStack: boolean;
|
||||
private currentTextEditorState: TextEditorState;
|
||||
private currentTextEditorState: TextEditorState | null;
|
||||
|
||||
private lastEditLocation: IStackEntry;
|
||||
|
||||
private history: (IEditorInput | IResourceInput)[];
|
||||
private history: Array<IEditorInput | IResourceInput>;
|
||||
private recentlyClosedFiles: IRecentlyClosedFile[];
|
||||
private loaded: boolean;
|
||||
private resourceFilter: ResourceGlobMatcher;
|
||||
@@ -125,18 +126,19 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
private canNavigateBackContextKey: IContextKey<boolean>;
|
||||
private canNavigateForwardContextKey: IContextKey<boolean>;
|
||||
private canNavigateToLastEditLocationContextKey: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
@IEditorService private editorService: EditorServiceImpl,
|
||||
@IEditorGroupsService private editorGroupService: IEditorGroupsService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IStorageService private storageService: IStorageService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IWindowsService private windowService: IWindowsService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@IPartService private partService: IPartService,
|
||||
@IContextKeyService private contextKeyService: IContextKeyService
|
||||
@IEditorService private readonly editorService: EditorServiceImpl,
|
||||
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IWindowsService private readonly windowService: IWindowsService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IPartService private readonly partService: IPartService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -144,6 +146,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
this.canNavigateBackContextKey = (new RawContextKey<boolean>('canNavigateBack', false)).bindTo(this.contextKeyService);
|
||||
this.canNavigateForwardContextKey = (new RawContextKey<boolean>('canNavigateForward', false)).bindTo(this.contextKeyService);
|
||||
this.canNavigateToLastEditLocationContextKey = (new RawContextKey<boolean>('canNavigateToLastEditLocation', false)).bindTo(this.contextKeyService);
|
||||
|
||||
this.fileInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorInputExtensions.EditorInputFactories).getFileInputFactory();
|
||||
|
||||
@@ -154,7 +157,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
this.loaded = false;
|
||||
this.resourceFilter = this._register(instantiationService.createInstance(
|
||||
ResourceGlobMatcher,
|
||||
(root: URI) => this.getExcludes(root),
|
||||
(root?: URI) => this.getExcludes(root),
|
||||
(event: IConfigurationChangeEvent) => event.affectsConfiguration(FILES_EXCLUDE_CONFIG) || event.affectsConfiguration('search.exclude')
|
||||
));
|
||||
|
||||
@@ -162,9 +165,9 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
private getExcludes(root?: URI): IExpression {
|
||||
const scope = root ? { resource: root } : void 0;
|
||||
const scope = root ? { resource: root } : undefined;
|
||||
|
||||
return getExcludes(this.configurationService.getValue<ISearchConfiguration>(scope));
|
||||
return getExcludes(scope ? this.configurationService.getValue<ISearchConfiguration>(scope) : this.configurationService.getValue<ISearchConfiguration>())!;
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
@@ -190,7 +193,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
// Remember as last active editor (can be undefined if none opened)
|
||||
this.lastActiveEditor = activeControl ? { editor: activeControl.input, groupId: activeControl.group.id } : void 0;
|
||||
this.lastActiveEditor = activeControl && activeControl.input && activeControl.group ? { editor: activeControl.input, groupId: activeControl.group.id } : undefined;
|
||||
|
||||
// Dispose old listeners
|
||||
dispose(this.activeEditorListeners);
|
||||
@@ -207,24 +210,27 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
// Debounce the event with a timeout of 0ms so that multiple calls to
|
||||
// editor.setSelection() are folded into one. We do not want to record
|
||||
// subsequent history navigations for such API calls.
|
||||
this.activeEditorListeners.push(debounceEvent(activeTextEditorWidget.onDidChangeCursorPosition, (last, event) => event, 0)((event => {
|
||||
this.activeEditorListeners.push(Event.debounce(activeTextEditorWidget.onDidChangeCursorPosition, (last, event) => event, 0)((event => {
|
||||
this.handleEditorSelectionChangeEvent(activeControl, event);
|
||||
})));
|
||||
|
||||
// Track the last edit location by tracking model content change events
|
||||
// Use a debouncer to make sure to capture the correct cursor position
|
||||
// after the model content has changed.
|
||||
this.activeEditorListeners.push(debounceEvent(activeTextEditorWidget.onDidChangeModelContent, (last, event) => event, 0)((event => {
|
||||
this.lastEditLocation = { input: activeEditor };
|
||||
this.activeEditorListeners.push(Event.debounce(activeTextEditorWidget.onDidChangeModelContent, (last, event) => event, 0)((event => this.rememberLastEditLocation(activeEditor, activeTextEditorWidget))));
|
||||
}
|
||||
}
|
||||
|
||||
const position = activeTextEditorWidget.getPosition();
|
||||
if (position) {
|
||||
this.lastEditLocation.selection = {
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column
|
||||
};
|
||||
}
|
||||
})));
|
||||
private rememberLastEditLocation(activeEditor: IEditorInput, activeTextEditorWidget: ICodeEditor): void {
|
||||
this.lastEditLocation = { input: activeEditor };
|
||||
this.canNavigateToLastEditLocationContextKey.set(true);
|
||||
|
||||
const position = activeTextEditorWidget.getPosition();
|
||||
if (position) {
|
||||
this.lastEditLocation.selection = {
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: position.column
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,9 +256,9 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
// Track closing of editor to support to reopen closed editors (unless editor was replaced)
|
||||
if (!event.replaced) {
|
||||
const resource = event.editor ? event.editor.getResource() : void 0;
|
||||
const resource = event.editor ? event.editor.getResource() : undefined;
|
||||
const supportsReopen = resource && this.fileService.canHandleResource(resource); // we only support file'ish things to reopen
|
||||
if (supportsReopen) {
|
||||
if (resource && supportsReopen) {
|
||||
|
||||
// Remove all inputs matching and add as last recently closed
|
||||
this.removeFromRecentlyClosedFiles(event.editor);
|
||||
@@ -401,16 +407,10 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
private navigate(acrossEditors?: boolean): void {
|
||||
this.navigatingInStack = true;
|
||||
|
||||
this.doNavigate(this.stack[this.index], !acrossEditors).then(() => {
|
||||
this.navigatingInStack = false;
|
||||
}, error => {
|
||||
this.navigatingInStack = false;
|
||||
|
||||
onUnexpectedError(error);
|
||||
});
|
||||
this.doNavigate(this.stack[this.index], !acrossEditors).finally(() => this.navigatingInStack = false);
|
||||
}
|
||||
|
||||
private doNavigate(location: IStackEntry, withSelection: boolean): Thenable<IBaseEditor> {
|
||||
private doNavigate(location: IStackEntry, withSelection: boolean): Promise<IBaseEditor> {
|
||||
const options: ITextEditorOptions = {
|
||||
revealIfOpened: true // support to navigate across editor groups
|
||||
};
|
||||
@@ -439,7 +439,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
private handleEditorEventInHistory(editor?: IBaseEditor): void {
|
||||
const input = editor ? editor.input : void 0;
|
||||
const input = editor ? editor.input : undefined;
|
||||
|
||||
// Ensure we have at least a name to show and not configured to exclude input
|
||||
if (!input || !input.getName() || !this.include(input)) {
|
||||
@@ -456,7 +456,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
// Respect max entries setting
|
||||
if (this.history.length > HistoryService.MAX_HISTORY_ITEMS) {
|
||||
this.clearOnEditorDispose(this.history.pop(), this.editorHistoryListeners);
|
||||
this.clearOnEditorDispose(this.history.pop()!, this.editorHistoryListeners);
|
||||
}
|
||||
|
||||
// Remove this from the history unless the history input is a resource
|
||||
@@ -467,7 +467,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
private onEditorDispose(editor: EditorInput, listener: Function, mapEditorToDispose: Map<EditorInput, IDisposable[]>): void {
|
||||
const toDispose = once(editor.onDispose)(() => listener());
|
||||
const toDispose = Event.once(editor.onDispose)(() => listener());
|
||||
|
||||
let disposables = mapEditorToDispose.get(editor);
|
||||
if (!disposables) {
|
||||
@@ -480,7 +480,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
private clearOnEditorDispose(editor: IEditorInput | IResourceInput | FileChangesEvent, mapEditorToDispose: Map<EditorInput, IDisposable[]>): void {
|
||||
if (editor instanceof EditorInput) {
|
||||
const disposables = this.editorHistoryListeners.get(editor);
|
||||
const disposables = mapEditorToDispose.get(editor);
|
||||
if (disposables) {
|
||||
dispose(disposables);
|
||||
mapEditorToDispose.delete(editor);
|
||||
@@ -541,15 +541,15 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
});
|
||||
}
|
||||
|
||||
private handleEditorEventInStack(control: IBaseEditor, event?: ICursorPositionChangedEvent): void {
|
||||
const codeEditor = control ? getCodeEditor(control.getControl()) : void 0;
|
||||
private handleEditorEventInStack(control: IBaseEditor | undefined, event?: ICursorPositionChangedEvent): void {
|
||||
const codeEditor = control ? getCodeEditor(control.getControl()) : undefined;
|
||||
|
||||
// treat editor changes that happen as part of stack navigation specially
|
||||
// we do not want to add a new stack entry as a matter of navigating the
|
||||
// stack but we need to keep our currentTextEditorState up to date with
|
||||
// the navigtion that occurs.
|
||||
if (this.navigatingInStack) {
|
||||
if (codeEditor && control.input) {
|
||||
if (codeEditor && control && control.input) {
|
||||
this.currentTextEditorState = new TextEditorState(control.input, codeEditor.getSelection());
|
||||
} else {
|
||||
this.currentTextEditorState = null; // we navigated to a non text editor
|
||||
@@ -560,7 +560,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
else {
|
||||
|
||||
// navigation inside text editor
|
||||
if (codeEditor && control.input) {
|
||||
if (codeEditor && control && control.input) {
|
||||
this.handleTextEditorEvent(control, codeEditor, event);
|
||||
}
|
||||
|
||||
@@ -576,6 +576,10 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
private handleTextEditorEvent(editor: IBaseEditor, editorControl: IEditor, event?: ICursorPositionChangedEvent): void {
|
||||
if (!editor.input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateCandidate = new TextEditorState(editor.input, editorControl.getSelection());
|
||||
|
||||
// Add to stack if we dont have a current state or this new state justifies a push
|
||||
@@ -593,6 +597,10 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
private handleNonTextEditorEvent(editor: IBaseEditor): void {
|
||||
if (!editor.input) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentStack = this.stack[this.index];
|
||||
if (currentStack && this.matches(editor.input, currentStack.input)) {
|
||||
return; // do not push same editor input again
|
||||
@@ -660,7 +668,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
// Check for limit
|
||||
if (this.stack.length > HistoryService.MAX_STACK_ITEMS) {
|
||||
removedEntries.push(this.stack.shift()); // remove first
|
||||
removedEntries.push(this.stack.shift()!); // remove first
|
||||
if (this.lastIndex >= 0) {
|
||||
this.lastIndex--;
|
||||
}
|
||||
@@ -695,7 +703,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((!selectionA && selectionB) || (selectionA && !selectionB)) {
|
||||
if (!selectionA || !selectionB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -797,7 +805,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
return resourceInput && resourceInput.resource.toString() === resource.toString();
|
||||
}
|
||||
|
||||
getHistory(): (IEditorInput | IResourceInput)[] {
|
||||
getHistory(): Array<IEditorInput | IResourceInput> {
|
||||
this.ensureHistoryLoaded();
|
||||
|
||||
return this.history.slice(0);
|
||||
@@ -818,7 +826,7 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);
|
||||
|
||||
const entries: ISerializedEditorHistoryEntry[] = this.history.map(input => {
|
||||
const entries: ISerializedEditorHistoryEntry[] = coalesce(this.history.map(input => {
|
||||
|
||||
// Editor input: try via factory
|
||||
if (input instanceof EditorInput) {
|
||||
@@ -836,8 +844,8 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
return { resourceJSON: (input as IResourceInput).resource.toJSON() } as ISerializedEditorHistoryEntry;
|
||||
}
|
||||
|
||||
return void 0;
|
||||
}).filter(serialized => !!serialized);
|
||||
return undefined;
|
||||
}));
|
||||
|
||||
this.storageService.store(HistoryService.STORAGE_KEY, JSON.stringify(entries), StorageScope.WORKSPACE);
|
||||
}
|
||||
@@ -847,23 +855,23 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
|
||||
const entriesRaw = this.storageService.get(HistoryService.STORAGE_KEY, StorageScope.WORKSPACE);
|
||||
if (entriesRaw) {
|
||||
entries = JSON.parse(entriesRaw).filter((entry: object) => !!entry);
|
||||
entries = coalesce(JSON.parse(entriesRaw));
|
||||
}
|
||||
|
||||
const registry = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories);
|
||||
|
||||
this.history = entries.map(entry => {
|
||||
this.history = coalesce(entries.map(entry => {
|
||||
try {
|
||||
return this.safeLoadHistoryEntry(registry, entry);
|
||||
} catch (error) {
|
||||
onUnexpectedError(error);
|
||||
|
||||
return void 0; // https://github.com/Microsoft/vscode/issues/60960
|
||||
return undefined; // https://github.com/Microsoft/vscode/issues/60960
|
||||
}
|
||||
}).filter(input => !!input);
|
||||
}));
|
||||
}
|
||||
|
||||
private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceInput {
|
||||
private safeLoadHistoryEntry(registry: IEditorInputFactoryRegistry, entry: ISerializedEditorHistoryEntry): IEditorInput | IResourceInput | undefined {
|
||||
const serializedEditorHistoryEntry = entry as ISerializedEditorHistoryEntry;
|
||||
|
||||
// File resource: via URI.revive()
|
||||
@@ -885,15 +893,15 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getLastActiveWorkspaceRoot(schemeFilter?: string): URI {
|
||||
getLastActiveWorkspaceRoot(schemeFilter?: string): URI | undefined {
|
||||
|
||||
// No Folder: return early
|
||||
const folders = this.contextService.getWorkspace().folders;
|
||||
if (folders.length === 0) {
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Single Folder: return early
|
||||
@@ -903,13 +911,12 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
return resource;
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Multiple folders: find the last active one
|
||||
const history = this.getHistory();
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const input = history[i];
|
||||
for (const input of history) {
|
||||
if (input instanceof EditorInput) {
|
||||
continue;
|
||||
}
|
||||
@@ -926,22 +933,20 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
|
||||
// fallback to first workspace matching scheme filter if any
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const resource = folders[i].uri;
|
||||
for (const folder of folders) {
|
||||
const resource = folder.uri;
|
||||
if (!schemeFilter || resource.scheme === schemeFilter) {
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getLastActiveFile(schemeFilter: string): URI {
|
||||
getLastActiveFile(schemeFilter: string): URI | undefined {
|
||||
const history = this.getHistory();
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
let resource: URI;
|
||||
|
||||
const input = history[i];
|
||||
for (const input of history) {
|
||||
let resource: URI | null;
|
||||
if (input instanceof EditorInput) {
|
||||
resource = toResource(input, { filter: schemeFilter });
|
||||
} else {
|
||||
@@ -953,6 +958,6 @@ export class HistoryService extends Disposable implements IHistoryService {
|
||||
}
|
||||
}
|
||||
|
||||
return void 0;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ export const IWorkbenchIssueService = createDecorator<IWorkbenchIssueService>('w
|
||||
|
||||
export interface IWorkbenchIssueService {
|
||||
_serviceBrand: any;
|
||||
openReporter(dataOverrides?: Partial<IssueReporterData>): Thenable<void>;
|
||||
openProcessExplorer(): Thenable<void>;
|
||||
openReporter(dataOverrides?: Partial<IssueReporterData>): Promise<void>;
|
||||
openProcessExplorer(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -7,26 +7,26 @@ import { IssueReporterStyles, IIssueService, IssueReporterData, ProcessExplorerD
|
||||
import { ITheme, IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground, textLinkActiveForeground } 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 { IExtensionManagementService, IExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { webFrame } from 'electron';
|
||||
import { assign } from 'vs/base/common/objects';
|
||||
import { IWorkbenchIssueService } from 'vs/workbench/services/issue/common/issue';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { ExtensionType } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export class WorkbenchIssueService implements IWorkbenchIssueService {
|
||||
_serviceBrand: any;
|
||||
|
||||
constructor(
|
||||
@IIssueService private issueService: IIssueService,
|
||||
@IThemeService private themeService: IThemeService,
|
||||
@IExtensionManagementService private extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService,
|
||||
@IWindowService private windowService: IWindowService
|
||||
) {
|
||||
}
|
||||
@IIssueService private readonly issueService: IIssueService,
|
||||
@IThemeService private readonly themeService: IThemeService,
|
||||
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
|
||||
@IExtensionEnablementService private readonly extensionEnablementService: IExtensionEnablementService,
|
||||
@IWindowService private readonly windowService: IWindowService
|
||||
) { }
|
||||
|
||||
openReporter(dataOverrides: Partial<IssueReporterData> = {}): Promise<void> {
|
||||
return this.extensionManagementService.getInstalled(LocalExtensionType.User).then(extensions => {
|
||||
return this.extensionManagementService.getInstalled(ExtensionType.User).then(extensions => {
|
||||
const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled(extension));
|
||||
const extensionData: IssueReporterExtensionData[] = enabledExtensions.map(extension => {
|
||||
const { manifest } = extension;
|
||||
@@ -57,17 +57,17 @@ export class WorkbenchIssueService implements IWorkbenchIssueService {
|
||||
});
|
||||
}
|
||||
|
||||
openProcessExplorer(): Thenable<void> {
|
||||
openProcessExplorer(): Promise<void> {
|
||||
const theme = this.themeService.getTheme();
|
||||
const data: ProcessExplorerData = {
|
||||
pid: this.windowService.getConfiguration().mainPid,
|
||||
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()
|
||||
backgroundColor: getColor(theme, editorBackground),
|
||||
color: getColor(theme, editorForeground),
|
||||
hoverBackground: getColor(theme, listHoverBackground),
|
||||
hoverForeground: getColor(theme, listHoverForeground),
|
||||
highlightForeground: getColor(theme, listHighlightForeground),
|
||||
}
|
||||
};
|
||||
return this.issueService.openProcessExplorer(data);
|
||||
@@ -76,20 +76,26 @@ export class WorkbenchIssueService implements IWorkbenchIssueService {
|
||||
|
||||
export function getIssueReporterStyles(theme: ITheme): IssueReporterStyles {
|
||||
return {
|
||||
backgroundColor: theme.getColor(SIDE_BAR_BACKGROUND) && theme.getColor(SIDE_BAR_BACKGROUND).toString(),
|
||||
color: theme.getColor(foreground).toString(),
|
||||
textLinkColor: theme.getColor(textLinkForeground) && theme.getColor(textLinkForeground).toString(),
|
||||
textLinkActiveForeground: theme.getColor(textLinkActiveForeground) && theme.getColor(textLinkActiveForeground).toString(),
|
||||
inputBackground: theme.getColor(inputBackground) && theme.getColor(inputBackground).toString(),
|
||||
inputForeground: theme.getColor(inputForeground) && theme.getColor(inputForeground).toString(),
|
||||
inputBorder: theme.getColor(inputBorder) && theme.getColor(inputBorder).toString(),
|
||||
inputActiveBorder: theme.getColor(inputActiveOptionBorder) && theme.getColor(inputActiveOptionBorder).toString(),
|
||||
inputErrorBorder: theme.getColor(inputValidationErrorBorder) && theme.getColor(inputValidationErrorBorder).toString(),
|
||||
buttonBackground: theme.getColor(buttonBackground) && theme.getColor(buttonBackground).toString(),
|
||||
buttonForeground: theme.getColor(buttonForeground) && theme.getColor(buttonForeground).toString(),
|
||||
buttonHoverBackground: theme.getColor(buttonHoverBackground) && theme.getColor(buttonHoverBackground).toString(),
|
||||
sliderActiveColor: theme.getColor(scrollbarSliderActiveBackground) && theme.getColor(scrollbarSliderActiveBackground).toString(),
|
||||
sliderBackgroundColor: theme.getColor(scrollbarSliderBackground) && theme.getColor(scrollbarSliderBackground).toString(),
|
||||
sliderHoverColor: theme.getColor(scrollbarSliderHoverBackground) && theme.getColor(scrollbarSliderHoverBackground).toString()
|
||||
backgroundColor: getColor(theme, SIDE_BAR_BACKGROUND),
|
||||
color: getColor(theme, foreground),
|
||||
textLinkColor: getColor(theme, textLinkForeground),
|
||||
textLinkActiveForeground: getColor(theme, textLinkActiveForeground),
|
||||
inputBackground: getColor(theme, inputBackground),
|
||||
inputForeground: getColor(theme, inputForeground),
|
||||
inputBorder: getColor(theme, inputBorder),
|
||||
inputActiveBorder: getColor(theme, inputActiveOptionBorder),
|
||||
inputErrorBorder: getColor(theme, inputValidationErrorBorder),
|
||||
buttonBackground: getColor(theme, buttonBackground),
|
||||
buttonForeground: getColor(theme, buttonForeground),
|
||||
buttonHoverBackground: getColor(theme, buttonHoverBackground),
|
||||
sliderActiveColor: getColor(theme, scrollbarSliderActiveBackground),
|
||||
sliderBackgroundColor: getColor(theme, scrollbarSliderBackground),
|
||||
sliderHoverColor: getColor(theme, scrollbarSliderHoverBackground),
|
||||
};
|
||||
}
|
||||
|
||||
function getColor(theme: ITheme, key: string): string | undefined {
|
||||
const color = theme.getColor(key);
|
||||
return color ? color.toString() : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,21 +13,25 @@ interface IJSONValidationExtensionPoint {
|
||||
url: string;
|
||||
}
|
||||
|
||||
let configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IJSONValidationExtensionPoint[]>('jsonValidation', [], {
|
||||
description: nls.localize('contributes.jsonValidation', 'Contributes json schema configuration.'),
|
||||
type: 'array',
|
||||
defaultSnippets: [{ body: [{ fileMatch: '${1:file.json}', url: '${2:url}' }] }],
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { fileMatch: '${1:file.json}', url: '${2:url}' } }],
|
||||
properties: {
|
||||
fileMatch: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.jsonValidation.fileMatch', 'The file pattern to match, for example "package.json" or "*.launch".'),
|
||||
},
|
||||
url: {
|
||||
description: nls.localize('contributes.jsonValidation.url', 'A schema URL (\'http:\', \'https:\') or relative path to the extension folder (\'./\').'),
|
||||
type: 'string'
|
||||
const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IJSONValidationExtensionPoint[]>({
|
||||
extensionPoint: 'jsonValidation',
|
||||
isDynamic: true,
|
||||
jsonSchema: {
|
||||
description: nls.localize('contributes.jsonValidation', 'Contributes json schema configuration.'),
|
||||
type: 'array',
|
||||
defaultSnippets: [{ body: [{ fileMatch: '${1:file.json}', url: '${2:url}' }] }],
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { fileMatch: '${1:file.json}', url: '${2:url}' } }],
|
||||
properties: {
|
||||
fileMatch: {
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.jsonValidation.fileMatch', 'The file pattern to match, for example "package.json" or "*.launch".'),
|
||||
},
|
||||
url: {
|
||||
description: nls.localize('contributes.jsonValidation.url', 'A schema URL (\'http:\', \'https:\') or relative path to the extension folder (\'./\').'),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,10 +41,10 @@ export class JSONValidationExtensionPoint {
|
||||
|
||||
constructor() {
|
||||
configurationExtPoint.setHandler((extensions) => {
|
||||
for (let i = 0; i < extensions.length; i++) {
|
||||
const extensionValue = <IJSONValidationExtensionPoint[]>extensions[i].value;
|
||||
const collector = extensions[i].collector;
|
||||
const extensionLocation = extensions[i].description.extensionLocation;
|
||||
for (const extension of extensions) {
|
||||
const extensionValue = <IJSONValidationExtensionPoint[]>extension.value;
|
||||
const collector = extension.collector;
|
||||
const extensionLocation = extension.description.extensionLocation;
|
||||
|
||||
if (!extensionValue || !Array.isArray(extensionValue)) {
|
||||
collector.error(nls.localize('invalid.jsonValidation', "'configuration.jsonValidation' must be a array"));
|
||||
|
||||
@@ -32,11 +32,11 @@ export interface IKeybindingEditingService {
|
||||
|
||||
_serviceBrand: ServiceIdentifier<any>;
|
||||
|
||||
editKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): Thenable<void>;
|
||||
editKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): Promise<void>;
|
||||
|
||||
removeKeybinding(keybindingItem: ResolvedKeybindingItem): Thenable<void>;
|
||||
removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;
|
||||
|
||||
resetKeybinding(keybindingItem: ResolvedKeybindingItem): Thenable<void>;
|
||||
resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void>;
|
||||
}
|
||||
|
||||
export class KeybindingsEditingService extends Disposable implements IKeybindingEditingService {
|
||||
@@ -47,29 +47,29 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
|
||||
private resource: URI = URI.file(this.environmentService.appKeybindingsPath);
|
||||
|
||||
constructor(
|
||||
@ITextModelService private textModelResolverService: ITextModelService,
|
||||
@ITextFileService private textFileService: ITextFileService,
|
||||
@IFileService private fileService: IFileService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@IEnvironmentService private environmentService: IEnvironmentService
|
||||
@ITextModelService private readonly textModelResolverService: ITextModelService,
|
||||
@ITextFileService private readonly textFileService: ITextFileService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService
|
||||
) {
|
||||
super();
|
||||
this.queue = new Queue<void>();
|
||||
}
|
||||
|
||||
editKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): Thenable<void> {
|
||||
editKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): Promise<void> {
|
||||
return this.queue.queue(() => this.doEditKeybinding(key, keybindingItem)); // queue up writes to prevent race conditions
|
||||
}
|
||||
|
||||
resetKeybinding(keybindingItem: ResolvedKeybindingItem): Thenable<void> {
|
||||
resetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
|
||||
return this.queue.queue(() => this.doResetKeybinding(keybindingItem)); // queue up writes to prevent race conditions
|
||||
}
|
||||
|
||||
removeKeybinding(keybindingItem: ResolvedKeybindingItem): Thenable<void> {
|
||||
removeKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
|
||||
return this.queue.queue(() => this.doRemoveKeybinding(keybindingItem)); // queue up writes to prevent race conditions
|
||||
}
|
||||
|
||||
private doEditKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): Thenable<void> {
|
||||
private doEditKeybinding(key: string, keybindingItem: ResolvedKeybindingItem): Promise<void> {
|
||||
return this.resolveAndValidate()
|
||||
.then(reference => {
|
||||
const model = reference.object.textEditorModel;
|
||||
@@ -83,7 +83,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
|
||||
});
|
||||
}
|
||||
|
||||
private doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): Thenable<void> {
|
||||
private doRemoveKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
|
||||
return this.resolveAndValidate()
|
||||
.then(reference => {
|
||||
const model = reference.object.textEditorModel;
|
||||
@@ -96,7 +96,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
|
||||
});
|
||||
}
|
||||
|
||||
private doResetKeybinding(keybindingItem: ResolvedKeybindingItem): Thenable<void> {
|
||||
private doResetKeybinding(keybindingItem: ResolvedKeybindingItem): Promise<void> {
|
||||
return this.resolveAndValidate()
|
||||
.then(reference => {
|
||||
const model = reference.object.textEditorModel;
|
||||
@@ -108,7 +108,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
|
||||
});
|
||||
}
|
||||
|
||||
private save(): Thenable<any> {
|
||||
private save(): Promise<any> {
|
||||
return this.textFileService.save(this.resource);
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
|
||||
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
|
||||
const userKeybindingEntryIndex = this.findUserKeybindingEntryIndex(keybindingItem, userKeybindingEntries);
|
||||
if (userKeybindingEntryIndex !== -1) {
|
||||
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex], void 0, { tabSize, insertSpaces, eol })[0], model);
|
||||
this.applyEditsToBuffer(setProperty(model.getValue(), [userKeybindingEntryIndex], undefined, { tabSize, insertSpaces, eol })[0], model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
|
||||
const userKeybindingEntries = <IUserFriendlyKeybinding[]>json.parse(model.getValue());
|
||||
const indices = this.findUnassignedDefaultKeybindingEntryIndex(keybindingItem, userKeybindingEntries).reverse();
|
||||
for (const index of indices) {
|
||||
this.applyEditsToBuffer(setProperty(model.getValue(), [index], void 0, { tabSize, insertSpaces, eol })[0], model);
|
||||
this.applyEditsToBuffer(setProperty(model.getValue(), [index], undefined, { tabSize, insertSpaces, eol })[0], model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,16 +197,16 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
|
||||
}
|
||||
|
||||
|
||||
private resolveModelReference(): Thenable<IReference<ITextEditorModel>> {
|
||||
private resolveModelReference(): Promise<IReference<ITextEditorModel>> {
|
||||
return this.fileService.existsFile(this.resource)
|
||||
.then(exists => {
|
||||
const EOL = this.configurationService.getValue('files', { overrideIdentifier: 'json' })['eol'];
|
||||
const result: Thenable<any> = exists ? Promise.resolve(null) : this.fileService.updateContent(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' });
|
||||
const result: Promise<any> = exists ? Promise.resolve(null) : this.fileService.updateContent(this.resource, this.getEmptyContent(EOL), { encoding: 'utf8' });
|
||||
return result.then(() => this.textModelResolverService.createModelReference(this.resource));
|
||||
});
|
||||
}
|
||||
|
||||
private resolveAndValidate(): Thenable<IReference<ITextEditorModel>> {
|
||||
private resolveAndValidate(): Promise<IReference<ITextEditorModel>> {
|
||||
|
||||
// Target cannot be dirty if not writing into buffer
|
||||
if (this.textFileService.isDirty(this.resource)) {
|
||||
|
||||
@@ -29,13 +29,14 @@ export class CachedKeyboardMapper implements IKeyboardMapper {
|
||||
}
|
||||
|
||||
public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] {
|
||||
let hashCode = keybinding.getHashCode();
|
||||
if (!this._cache.has(hashCode)) {
|
||||
let r = this._actual.resolveKeybinding(keybinding);
|
||||
const hashCode = keybinding.getHashCode();
|
||||
const resolved = this._cache.get(hashCode);
|
||||
if (!resolved) {
|
||||
const r = this._actual.resolveKeybinding(keybinding);
|
||||
this._cache.set(hashCode, r);
|
||||
return r;
|
||||
}
|
||||
return this._cache.get(hashCode);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
|
||||
|
||||
@@ -445,11 +445,11 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper {
|
||||
/**
|
||||
* UI label for a ScanCode.
|
||||
*/
|
||||
private readonly _scanCodeToLabel: (string | null)[] = [];
|
||||
private readonly _scanCodeToLabel: Array<string | null> = [];
|
||||
/**
|
||||
* Dispatching string for a ScanCode.
|
||||
*/
|
||||
private readonly _scanCodeToDispatch: (string | null)[] = [];
|
||||
private readonly _scanCodeToDispatch: Array<string | null> = [];
|
||||
|
||||
constructor(isUSStandard: boolean, rawMappings: IMacLinuxKeyboardMapping, OS: OperatingSystem) {
|
||||
this._isUSStandard = isUSStandard;
|
||||
@@ -800,7 +800,7 @@ export class MacLinuxKeyboardMapper implements IKeyboardMapper {
|
||||
for (let i = 0, len = kbCombos.length; i < len; i++) {
|
||||
const kbCombo = kbCombos[i];
|
||||
// find out the priority of this scan code for this key code
|
||||
let colPriority = '-';
|
||||
let colPriority: string;
|
||||
|
||||
const scanCodeCombos = this._scanCodeKeyCodeMapper.lookupKeyCodeCombo(kbCombo);
|
||||
if (scanCodeCombos.length === 1) {
|
||||
|
||||
@@ -303,7 +303,7 @@ export class WindowsKeyboardMapper implements IKeyboardMapper {
|
||||
public readonly isUSStandard: boolean;
|
||||
private readonly _codeInfo: IScanCodeMapping[];
|
||||
private readonly _scanCodeToKeyCode: KeyCode[];
|
||||
private readonly _keyCodeToLabel: (string | null)[] = [];
|
||||
private readonly _keyCodeToLabel: Array<string | null> = [];
|
||||
private readonly _keyCodeExists: boolean[];
|
||||
|
||||
constructor(isUSStandard: boolean, rawMappings: IWindowsKeyboardMapping) {
|
||||
@@ -460,9 +460,8 @@ export class WindowsKeyboardMapper implements IKeyboardMapper {
|
||||
const mapping = this._codeInfo[scanCode];
|
||||
const strCode = ScanCodeUtils.toString(scanCode);
|
||||
|
||||
let mods = [0b000, 0b010, 0b101, 0b111];
|
||||
for (let modIndex = 0; modIndex < mods.length; modIndex++) {
|
||||
const mod = mods[modIndex];
|
||||
const mods = [0b000, 0b010, 0b101, 0b111];
|
||||
for (const mod of mods) {
|
||||
const ctrlKey = (mod & 0b001) ? true : false;
|
||||
const shiftKey = (mod & 0b010) ? true : false;
|
||||
const altKey = (mod & 0b100) ? true : false;
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/c
|
||||
import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService';
|
||||
import { IKeybindingEvent, IKeyboardEvent, IUserFriendlyKeybinding, KeybindingSource } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
|
||||
import { IKeybindingItem, IKeybindingRule2, KeybindingRuleSource, KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { IKeybindingItem, IKeybindingRule2, KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
@@ -37,6 +37,7 @@ import { CachedKeyboardMapper, IKeyboardMapper } from 'vs/workbench/services/key
|
||||
import { MacLinuxFallbackKeyboardMapper } from 'vs/workbench/services/keybinding/common/macLinuxFallbackKeyboardMapper';
|
||||
import { IMacLinuxKeyboardMapping, MacLinuxKeyboardMapper, macLinuxKeyboardMappingEquals } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper';
|
||||
import { IWindowsKeyboardMapping, WindowsKeyboardMapper, windowsKeyboardMappingEquals } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
|
||||
export class KeyboardMapperFactory {
|
||||
public static readonly INSTANCE = new KeyboardMapperFactory();
|
||||
@@ -46,7 +47,7 @@ export class KeyboardMapperFactory {
|
||||
private _keyboardMapper: IKeyboardMapper | null;
|
||||
private _initialized: boolean;
|
||||
|
||||
private readonly _onDidChangeKeyboardMapper: Emitter<void> = new Emitter<void>();
|
||||
private readonly _onDidChangeKeyboardMapper = new Emitter<void>();
|
||||
public readonly onDidChangeKeyboardMapper: Event<void> = this._onDidChangeKeyboardMapper.event;
|
||||
|
||||
private constructor() {
|
||||
@@ -155,6 +156,7 @@ export class KeyboardMapperFactory {
|
||||
|
||||
interface ContributedKeyBinding {
|
||||
command: string;
|
||||
args?: any;
|
||||
key: string;
|
||||
when?: string;
|
||||
mac?: string;
|
||||
@@ -206,6 +208,9 @@ let keybindingType: IJSONSchema = {
|
||||
description: nls.localize('vscode.extension.contributes.keybindings.command', 'Identifier of the command to run when keybinding is triggered.'),
|
||||
type: 'string'
|
||||
},
|
||||
args: {
|
||||
description: nls.localize('vscode.extension.contributes.keybindings.args', "Arguments to pass to the command to execute.")
|
||||
},
|
||||
key: {
|
||||
description: nls.localize('vscode.extension.contributes.keybindings.key', 'Key or key sequence (separate keys with plus-sign and sequences with space, e.g Ctrl+O and Ctrl+L L for a chord).'),
|
||||
type: 'string'
|
||||
@@ -225,19 +230,23 @@ let keybindingType: IJSONSchema = {
|
||||
when: {
|
||||
description: nls.localize('vscode.extension.contributes.keybindings.when', 'Condition when the key is active.'),
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let keybindingsExtPoint = ExtensionsRegistry.registerExtensionPoint<ContributedKeyBinding | ContributedKeyBinding[]>('keybindings', [], {
|
||||
description: nls.localize('vscode.extension.contributes.keybindings', "Contributes keybindings."),
|
||||
oneOf: [
|
||||
keybindingType,
|
||||
{
|
||||
type: 'array',
|
||||
items: keybindingType
|
||||
}
|
||||
]
|
||||
const keybindingsExtPoint = ExtensionsRegistry.registerExtensionPoint<ContributedKeyBinding | ContributedKeyBinding[]>({
|
||||
isDynamic: true,
|
||||
extensionPoint: 'keybindings',
|
||||
jsonSchema: {
|
||||
description: nls.localize('vscode.extension.contributes.keybindings', "Contributes keybindings."),
|
||||
oneOf: [
|
||||
keybindingType,
|
||||
{
|
||||
type: 'array',
|
||||
items: keybindingType
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
export const enum DispatchConfig {
|
||||
@@ -266,7 +275,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IStatusbarService statusBarService: IStatusbarService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IWindowService private readonly windowService: IWindowService
|
||||
) {
|
||||
super(contextKeyService, commandService, telemetryService, notificationService, statusBarService);
|
||||
|
||||
@@ -294,15 +304,14 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
this.userKeybindings = this._register(new ConfigWatcher(environmentService.appKeybindingsPath, { defaultConfig: [], onError: error => onUnexpectedError(error) }));
|
||||
|
||||
keybindingsExtPoint.setHandler((extensions) => {
|
||||
let commandAdded = false;
|
||||
|
||||
let keybindings: IKeybindingRule2[] = [];
|
||||
for (let extension of extensions) {
|
||||
commandAdded = this._handleKeybindingsExtensionPointUser(extension.description.isBuiltin, extension.value, extension.collector) || commandAdded;
|
||||
this._handleKeybindingsExtensionPointUser(extension.description.isBuiltin, extension.value, extension.collector, keybindings);
|
||||
}
|
||||
|
||||
if (commandAdded) {
|
||||
this.updateResolver({ source: KeybindingSource.Default });
|
||||
}
|
||||
KeybindingsRegistry.setExtensionKeybindings(keybindings);
|
||||
this.updateResolver({ source: KeybindingSource.Default });
|
||||
});
|
||||
|
||||
this._register(this.userKeybindings.onDidUpdateConfiguration(event => this.updateResolver({
|
||||
@@ -367,13 +376,15 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
}
|
||||
|
||||
protected _documentHasFocus(): boolean {
|
||||
return document.hasFocus();
|
||||
// it is possible that the document has lost focus, but the
|
||||
// window is still focused, e.g. when a <webview> element
|
||||
// has focus
|
||||
return this.windowService.hasFocus;
|
||||
}
|
||||
|
||||
private _resolveKeybindingItems(items: IKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
|
||||
let result: ResolvedKeybindingItem[] = [], resultLen = 0;
|
||||
for (let i = 0, len = items.length; i < len; i++) {
|
||||
const item = items[i];
|
||||
for (const item of items) {
|
||||
const when = (item.when ? item.when.normalize() : null);
|
||||
const keybinding = item.keybinding;
|
||||
if (!keybinding) {
|
||||
@@ -381,8 +392,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
result[resultLen++] = new ResolvedKeybindingItem(null, item.command, item.commandArgs, when, isDefault);
|
||||
} else {
|
||||
const resolvedKeybindings = this.resolveKeybinding(keybinding);
|
||||
for (let j = 0; j < resolvedKeybindings.length; j++) {
|
||||
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybindings[j], item.command, item.commandArgs, when, isDefault);
|
||||
for (const resolvedKeybinding of resolvedKeybindings) {
|
||||
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -392,8 +403,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
|
||||
private _resolveUserKeybindingItems(items: IUserKeybindingItem[], isDefault: boolean): ResolvedKeybindingItem[] {
|
||||
let result: ResolvedKeybindingItem[] = [], resultLen = 0;
|
||||
for (let i = 0, len = items.length; i < len; i++) {
|
||||
const item = items[i];
|
||||
for (const item of items) {
|
||||
const when = (item.when ? item.when.normalize() : null);
|
||||
const firstPart = item.firstPart;
|
||||
const chordPart = item.chordPart;
|
||||
@@ -402,8 +412,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
result[resultLen++] = new ResolvedKeybindingItem(null, item.command, item.commandArgs, when, isDefault);
|
||||
} else {
|
||||
const resolvedKeybindings = this._keyboardMapper.resolveUserBinding(firstPart, chordPart);
|
||||
for (let j = 0; j < resolvedKeybindings.length; j++) {
|
||||
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybindings[j], item.command, item.commandArgs, when, isDefault);
|
||||
for (const resolvedKeybinding of resolvedKeybindings) {
|
||||
result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -442,28 +452,24 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
return this._keyboardMapper.resolveUserBinding(firstPart, chordPart);
|
||||
}
|
||||
|
||||
private _handleKeybindingsExtensionPointUser(isBuiltin: boolean, keybindings: ContributedKeyBinding | ContributedKeyBinding[], collector: ExtensionMessageCollector): boolean {
|
||||
private _handleKeybindingsExtensionPointUser(isBuiltin: boolean, keybindings: ContributedKeyBinding | ContributedKeyBinding[], collector: ExtensionMessageCollector, result: IKeybindingRule2[]): void {
|
||||
if (isContributedKeyBindingsArray(keybindings)) {
|
||||
let commandAdded = false;
|
||||
for (let i = 0, len = keybindings.length; i < len; i++) {
|
||||
commandAdded = this._handleKeybinding(isBuiltin, i + 1, keybindings[i], collector) || commandAdded;
|
||||
this._handleKeybinding(isBuiltin, i + 1, keybindings[i], collector, result);
|
||||
}
|
||||
return commandAdded;
|
||||
} else {
|
||||
return this._handleKeybinding(isBuiltin, 1, keybindings, collector);
|
||||
this._handleKeybinding(isBuiltin, 1, keybindings, collector, result);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleKeybinding(isBuiltin: boolean, idx: number, keybindings: ContributedKeyBinding, collector: ExtensionMessageCollector): boolean {
|
||||
private _handleKeybinding(isBuiltin: boolean, idx: number, keybindings: ContributedKeyBinding, collector: ExtensionMessageCollector, result: IKeybindingRule2[]): void {
|
||||
|
||||
let rejects: string[] = [];
|
||||
let commandAdded = false;
|
||||
|
||||
if (isValidContributedKeyBinding(keybindings, rejects)) {
|
||||
let rule = this._asCommandRule(isBuiltin, idx++, keybindings);
|
||||
if (rule) {
|
||||
KeybindingsRegistry.registerKeybindingRule2(rule, KeybindingRuleSource.Extension);
|
||||
commandAdded = true;
|
||||
result.push(rule);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,13 +481,11 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
rejects.join('\n')
|
||||
));
|
||||
}
|
||||
|
||||
return commandAdded;
|
||||
}
|
||||
|
||||
private _asCommandRule(isBuiltin: boolean, idx: number, binding: ContributedKeyBinding): IKeybindingRule2 | undefined {
|
||||
|
||||
let { command, when, key, mac, linux, win } = binding;
|
||||
let { command, args, when, key, mac, linux, win } = binding;
|
||||
|
||||
let weight: number;
|
||||
if (isBuiltin) {
|
||||
@@ -492,6 +496,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
|
||||
|
||||
let desc: IKeybindingRule2 = {
|
||||
id: command,
|
||||
args,
|
||||
when: ContextKeyExpr.deserialize(when),
|
||||
weight: weight,
|
||||
primary: KeybindingParser.parseKeybinding(key, OS),
|
||||
|
||||
@@ -111,11 +111,11 @@ suite('KeybindingsEditing', () => {
|
||||
teardown(() => {
|
||||
return new Promise<void>((c, e) => {
|
||||
if (testDir) {
|
||||
extfs.del(testDir, os.tmpdir(), () => c(null), () => c(null));
|
||||
extfs.del(testDir, os.tmpdir(), () => c(undefined), () => c(undefined));
|
||||
} else {
|
||||
c(null);
|
||||
c(undefined);
|
||||
}
|
||||
}).then(() => testDir = null);
|
||||
}).then(() => testDir = null!);
|
||||
});
|
||||
|
||||
test('errors cases - parse errors', () => {
|
||||
@@ -247,10 +247,10 @@ suite('KeybindingsEditing', () => {
|
||||
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);
|
||||
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);
|
||||
return new ResolvedKeybindingItem(keybinding ? new USLayoutResolvedKeybinding(keybinding, OS) : null, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : null, isDefault === undefined ? true : isDefault);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ suite('keybindingIO', () => {
|
||||
test('serialize/deserialize', () => {
|
||||
|
||||
function testOneSerialization(keybinding: number, expected: string, msg: string, OS: OperatingSystem): void {
|
||||
let usLayoutResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS), OS);
|
||||
let usLayoutResolvedKeybinding = new USLayoutResolvedKeybinding(createKeybinding(keybinding, OS)!, OS);
|
||||
let actualSerialized = usLayoutResolvedKeybinding.getUserSettingsLabel();
|
||||
assert.equal(actualSerialized, expected, expected + ' - ' + msg);
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ function toIResolvedKeybinding(kb: ResolvedKeybinding): IResolvedKeybinding {
|
||||
};
|
||||
}
|
||||
|
||||
export function assertResolveKeybinding(mapper: IKeyboardMapper, keybinding: Keybinding, expected: IResolvedKeybinding[]): void {
|
||||
let actual: IResolvedKeybinding[] = mapper.resolveKeybinding(keybinding).map(toIResolvedKeybinding);
|
||||
export function assertResolveKeybinding(mapper: IKeyboardMapper, keybinding: Keybinding | null, expected: IResolvedKeybinding[]): void {
|
||||
let actual: IResolvedKeybinding[] = mapper.resolveKeybinding(keybinding!).map(toIResolvedKeybinding);
|
||||
assert.deepEqual(actual, expected);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function assertResolveKeyboardEvent(mapper: IKeyboardMapper, keyboardEven
|
||||
assert.deepEqual(actual, expected);
|
||||
}
|
||||
|
||||
export function assertResolveUserBinding(mapper: IKeyboardMapper, firstPart: SimpleKeybinding | ScanCodeBinding, chordPart: SimpleKeybinding | ScanCodeBinding, expected: IResolvedKeybinding[]): void {
|
||||
export function assertResolveUserBinding(mapper: IKeyboardMapper, firstPart: SimpleKeybinding | ScanCodeBinding, chordPart: SimpleKeybinding | ScanCodeBinding | null, expected: IResolvedKeybinding[]): void {
|
||||
let actual: IResolvedKeybinding[] = mapper.resolveUserBinding(firstPart, chordPart).map(toIResolvedKeybinding);
|
||||
assert.deepEqual(actual, expected);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ suite('keyboardMapper - MAC fallback', () => {
|
||||
let mapper = new MacLinuxFallbackKeyboardMapper(OperatingSystem.Macintosh);
|
||||
|
||||
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Macintosh), expected);
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Macintosh)!, expected);
|
||||
}
|
||||
|
||||
test('resolveKeybinding Cmd+Z', () => {
|
||||
@@ -56,7 +56,7 @@ suite('keyboardMapper - MAC fallback', () => {
|
||||
altKey: false,
|
||||
metaKey: true,
|
||||
keyCode: KeyCode.KEY_Z,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: '⌘Z',
|
||||
@@ -96,7 +96,7 @@ suite('keyboardMapper - MAC fallback', () => {
|
||||
altKey: false,
|
||||
metaKey: true,
|
||||
keyCode: KeyCode.Meta,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: '⌘',
|
||||
@@ -116,7 +116,7 @@ suite('keyboardMapper - LINUX fallback', () => {
|
||||
let mapper = new MacLinuxFallbackKeyboardMapper(OperatingSystem.Linux);
|
||||
|
||||
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux), expected);
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux)!, expected);
|
||||
}
|
||||
|
||||
test('resolveKeybinding Ctrl+Z', () => {
|
||||
@@ -158,7 +158,7 @@ suite('keyboardMapper - LINUX fallback', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.KEY_Z,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+Z',
|
||||
@@ -215,7 +215,7 @@ suite('keyboardMapper - LINUX fallback', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.Ctrl,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+',
|
||||
|
||||
@@ -37,7 +37,7 @@ suite('keyboardMapper - MAC de_ch', () => {
|
||||
}
|
||||
|
||||
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Macintosh), expected);
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Macintosh)!, expected);
|
||||
}
|
||||
|
||||
test('kb => hw', () => {
|
||||
@@ -463,7 +463,7 @@ suite('keyboardMapper - LINUX de_ch', () => {
|
||||
}
|
||||
|
||||
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux), expected);
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux)!, expected);
|
||||
}
|
||||
|
||||
test('kb => hw', () => {
|
||||
@@ -808,7 +808,7 @@ suite('keyboardMapper - LINUX en_us', () => {
|
||||
});
|
||||
|
||||
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux), expected);
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux)!, expected);
|
||||
}
|
||||
|
||||
test('resolveKeybinding Ctrl+A', () => {
|
||||
@@ -1224,7 +1224,7 @@ suite('keyboardMapper', () => {
|
||||
test('issue #24064: NumLock/NumPad keys stopped working in 1.11 on Linux', () => {
|
||||
let mapper = new MacLinuxKeyboardMapper(false, {}, OperatingSystem.Linux);
|
||||
|
||||
function assertNumpadKeyboardEvent(keyCode: KeyCode, code: string, label: string, electronAccelerator: string, userSettingsLabel: string, dispatch: string): void {
|
||||
function assertNumpadKeyboardEvent(keyCode: KeyCode, code: string, label: string, electronAccelerator: string | null, userSettingsLabel: string, dispatch: string): void {
|
||||
assertResolveKeyboardEvent(
|
||||
mapper,
|
||||
{
|
||||
@@ -1251,7 +1251,7 @@ suite('keyboardMapper', () => {
|
||||
assertNumpadKeyboardEvent(KeyCode.DownArrow, 'Numpad2', 'DownArrow', 'Down', 'down', '[ArrowDown]');
|
||||
assertNumpadKeyboardEvent(KeyCode.PageDown, 'Numpad3', 'PageDown', 'PageDown', 'pagedown', '[PageDown]');
|
||||
assertNumpadKeyboardEvent(KeyCode.LeftArrow, 'Numpad4', 'LeftArrow', 'Left', 'left', '[ArrowLeft]');
|
||||
assertNumpadKeyboardEvent(KeyCode.Unknown, 'Numpad5', 'NumPad5', null, 'numpad5', '[Numpad5]');
|
||||
assertNumpadKeyboardEvent(KeyCode.Unknown, 'Numpad5', 'NumPad5', null!, 'numpad5', '[Numpad5]');
|
||||
assertNumpadKeyboardEvent(KeyCode.RightArrow, 'Numpad6', 'RightArrow', 'Right', 'right', '[ArrowRight]');
|
||||
assertNumpadKeyboardEvent(KeyCode.Home, 'Numpad7', 'Home', 'Home', 'home', '[Home]');
|
||||
assertNumpadKeyboardEvent(KeyCode.UpArrow, 'Numpad8', 'UpArrow', 'Up', 'up', '[ArrowUp]');
|
||||
@@ -1326,7 +1326,7 @@ suite('keyboardMapper - LINUX ru', () => {
|
||||
});
|
||||
|
||||
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux), expected);
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Linux)!, expected);
|
||||
}
|
||||
|
||||
test('resolveKeybinding Ctrl+S', () => {
|
||||
@@ -1396,7 +1396,7 @@ suite('keyboardMapper - MAC zh_hant', () => {
|
||||
});
|
||||
|
||||
function _assertResolveKeybinding(k: number, expected: IResolvedKeybinding[]): void {
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Macintosh), expected);
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Macintosh)!, expected);
|
||||
}
|
||||
|
||||
test('issue #28237 resolveKeybinding Cmd+C', () => {
|
||||
@@ -1427,7 +1427,7 @@ function _assertKeybindingTranslation(mapper: MacLinuxKeyboardMapper, OS: Operat
|
||||
|
||||
const runtimeKeybinding = createKeybinding(kb, OS);
|
||||
|
||||
const keybindingLabel = new USLayoutResolvedKeybinding(runtimeKeybinding, OS).getUserSettingsLabel();
|
||||
const keybindingLabel = new USLayoutResolvedKeybinding(runtimeKeybinding!, OS).getUserSettingsLabel();
|
||||
|
||||
const actualHardwareKeypresses = mapper.simpleKeybindingToScanCodeBinding(<SimpleKeybinding>runtimeKeybinding);
|
||||
if (actualHardwareKeypresses.length === 0) {
|
||||
|
||||
@@ -17,7 +17,8 @@ async function createKeyboardMapper(isUSStandard: boolean, file: string): Promis
|
||||
}
|
||||
|
||||
function _assertResolveKeybinding(mapper: WindowsKeyboardMapper, k: number, expected: IResolvedKeybinding[]): void {
|
||||
assertResolveKeybinding(mapper, createKeybinding(k, OperatingSystem.Windows), expected);
|
||||
const keyBinding = createKeybinding(k, OperatingSystem.Windows);
|
||||
assertResolveKeybinding(mapper, keyBinding!, expected);
|
||||
}
|
||||
|
||||
suite('keyboardMapper - WINDOWS de_ch', () => {
|
||||
@@ -73,7 +74,7 @@ suite('keyboardMapper - WINDOWS de_ch', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.KEY_Z,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+Z',
|
||||
@@ -112,7 +113,7 @@ suite('keyboardMapper - WINDOWS de_ch', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.US_CLOSE_SQUARE_BRACKET,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+^',
|
||||
@@ -255,7 +256,7 @@ suite('keyboardMapper - WINDOWS de_ch', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.Home,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+Home',
|
||||
@@ -295,7 +296,7 @@ suite('keyboardMapper - WINDOWS de_ch', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.Ctrl,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+',
|
||||
@@ -359,7 +360,7 @@ suite('keyboardMapper - WINDOWS en_us', () => {
|
||||
assertResolveUserBinding(
|
||||
mapper,
|
||||
new ScanCodeBinding(true, false, false, false, ScanCode.Comma),
|
||||
null,
|
||||
null!,
|
||||
[{
|
||||
label: 'Ctrl+,',
|
||||
ariaLabel: 'Control+,',
|
||||
@@ -381,7 +382,7 @@ suite('keyboardMapper - WINDOWS en_us', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.Ctrl,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+',
|
||||
@@ -417,7 +418,7 @@ suite('keyboardMapper - WINDOWS por_ptb', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.ABNT_C1,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+/',
|
||||
@@ -440,7 +441,7 @@ suite('keyboardMapper - WINDOWS por_ptb', () => {
|
||||
altKey: false,
|
||||
metaKey: false,
|
||||
keyCode: KeyCode.ABNT_C2,
|
||||
code: null
|
||||
code: null!
|
||||
},
|
||||
{
|
||||
label: 'Ctrl+.',
|
||||
|
||||
252
src/vs/workbench/services/label/common/labelService.ts
Normal file
252
src/vs/workbench/services/label/common/labelService.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace';
|
||||
import { isEqual, basenameOrAuthority, basename as resourceBasename } from 'vs/base/common/resources';
|
||||
import { isLinux, isWindows } from 'vs/base/common/platform';
|
||||
import { tildify, getPathLabel } from 'vs/base/common/labels';
|
||||
import { ltrim } from 'vs/base/common/strings';
|
||||
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION, toWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { isParent } from 'vs/platform/files/common/files';
|
||||
import { basename, dirname, join } from 'vs/base/common/paths';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IWindowService } from 'vs/platform/windows/common/windows';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting } from 'vs/platform/label/common/label';
|
||||
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { match } from 'vs/base/common/glob';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
|
||||
const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint<ResourceLabelFormatter[]>({
|
||||
extensionPoint: 'resourceLabelFormatters',
|
||||
isDynamic: true,
|
||||
jsonSchema: {
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters', 'Contributes resource label formatting rules.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['scheme', 'formatting'],
|
||||
properties: {
|
||||
scheme: {
|
||||
type: 'string',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.scheme', 'URI scheme on which to match the formatter on. For example "file". Simple glob patterns are supported.'),
|
||||
},
|
||||
authority: {
|
||||
type: 'string',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.authority', 'URI authority on which to match the formatter on. Simple glob patterns are supported.'),
|
||||
},
|
||||
formatting: {
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.formatting', "Rules for formatting uri resource labels."),
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.label', "Label rules to display. For example: myLabel:/${path}. ${path}, ${scheme} and ${authority} are supported as variables.")
|
||||
},
|
||||
separator: {
|
||||
type: 'string',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.separator', "Separator to be used in the uri label display. '/' or '\' as an example.")
|
||||
},
|
||||
tildify: {
|
||||
type: 'boolean',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.tildify', "Controls if the start of the uri label should be tildified when possible.")
|
||||
},
|
||||
workspaceSuffix: {
|
||||
type: 'string',
|
||||
description: localize('vscode.extension.contributes.resourceLabelFormatters.formatting.workspaceSuffix', "Suffix appended to the workspace label.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sepRegexp = /\//g;
|
||||
const labelMatchingRegexp = /\$\{scheme\}|\$\{authority\}|\$\{path\}/g;
|
||||
|
||||
function hasDriveLetter(path: string): boolean {
|
||||
return !!(isWindows && path && path[2] === ':');
|
||||
}
|
||||
|
||||
class ResourceLabelFormattersHandler implements IWorkbenchContribution {
|
||||
private formattersDisposables = new Map<ResourceLabelFormatter, IDisposable>();
|
||||
|
||||
constructor(@ILabelService labelService: ILabelService) {
|
||||
resourceLabelFormattersExtPoint.setHandler((extensions, delta) => {
|
||||
delta.added.forEach(added => added.value.forEach(formatter => {
|
||||
this.formattersDisposables.set(formatter, labelService.registerFormatter(formatter));
|
||||
}));
|
||||
delta.removed.forEach(removed => removed.value.forEach(formatter => {
|
||||
this.formattersDisposables.get(formatter)!.dispose();
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceLabelFormattersHandler, LifecyclePhase.Restored);
|
||||
|
||||
export class LabelService implements ILabelService {
|
||||
_serviceBrand: any;
|
||||
|
||||
private formatters: ResourceLabelFormatter[] = [];
|
||||
private readonly _onDidChangeFormatters = new Emitter<void>();
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IWindowService private readonly windowService: IWindowService
|
||||
) { }
|
||||
|
||||
get onDidChangeFormatters(): Event<void> {
|
||||
return this._onDidChangeFormatters.event;
|
||||
}
|
||||
|
||||
findFormatting(resource: URI): ResourceLabelFormatting | undefined {
|
||||
let bestResult: ResourceLabelFormatter | undefined;
|
||||
|
||||
this.formatters.forEach(formatter => {
|
||||
if (formatter.scheme === resource.scheme) {
|
||||
if (!bestResult && !formatter.authority) {
|
||||
bestResult = formatter;
|
||||
return;
|
||||
}
|
||||
if (!formatter.authority) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (match(formatter.authority, resource.authority) && (!bestResult || !bestResult.authority || formatter.authority.length > bestResult.authority.length)) {
|
||||
bestResult = formatter;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return bestResult ? bestResult.formatting : undefined;
|
||||
}
|
||||
|
||||
getUriLabel(resource: URI, options: { relative?: boolean, noPrefix?: boolean } = {}): string {
|
||||
const formatting = this.findFormatting(resource);
|
||||
if (!formatting) {
|
||||
return getPathLabel(resource.path, this.environmentService, options.relative ? this.contextService : undefined);
|
||||
}
|
||||
|
||||
if (options.relative) {
|
||||
const baseResource = this.contextService && this.contextService.getWorkspaceFolder(resource);
|
||||
if (baseResource) {
|
||||
let relativeLabel: string;
|
||||
if (isEqual(baseResource.uri, resource, !isLinux)) {
|
||||
relativeLabel = ''; // no label if resources are identical
|
||||
} else {
|
||||
const baseResourceLabel = this.formatUri(baseResource.uri, formatting, options.noPrefix);
|
||||
relativeLabel = ltrim(this.formatUri(resource, formatting, options.noPrefix).substring(baseResourceLabel.length), formatting.separator);
|
||||
}
|
||||
|
||||
const hasMultipleRoots = this.contextService.getWorkspace().folders.length > 1;
|
||||
if (hasMultipleRoots && !options.noPrefix) {
|
||||
const rootName = (baseResource && baseResource.name) ? baseResource.name : basenameOrAuthority(baseResource.uri);
|
||||
relativeLabel = relativeLabel ? (rootName + ' • ' + relativeLabel) : rootName; // always show root basename if there are multiple
|
||||
}
|
||||
|
||||
return relativeLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return this.formatUri(resource, formatting, options.noPrefix);
|
||||
}
|
||||
|
||||
getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string {
|
||||
if (!isWorkspaceIdentifier(workspace) && !isSingleFolderWorkspaceIdentifier(workspace)) {
|
||||
const identifier = toWorkspaceIdentifier(workspace);
|
||||
if (!identifier) {
|
||||
return '';
|
||||
}
|
||||
|
||||
workspace = identifier;
|
||||
}
|
||||
|
||||
// Workspace: Single Folder
|
||||
if (isSingleFolderWorkspaceIdentifier(workspace)) {
|
||||
// Folder on disk
|
||||
const label = options && options.verbose ? this.getUriLabel(workspace) : resourceBasename(workspace) || '/';
|
||||
if (workspace.scheme === Schemas.file) {
|
||||
return label;
|
||||
}
|
||||
|
||||
const formatting = this.findFormatting(workspace);
|
||||
const suffix = formatting && (typeof formatting.workspaceSuffix === 'string') ? formatting.workspaceSuffix : workspace.scheme;
|
||||
return suffix ? `${label} (${suffix})` : label;
|
||||
}
|
||||
|
||||
// Workspace: Untitled
|
||||
if (isParent(workspace.configPath, this.environmentService.workspacesHome, !isLinux /* ignore case */)) {
|
||||
return localize('untitledWorkspace', "Untitled (Workspace)");
|
||||
}
|
||||
|
||||
// Workspace: Saved
|
||||
const filename = basename(workspace.configPath);
|
||||
const workspaceName = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1);
|
||||
if (options && options.verbose) {
|
||||
return localize('workspaceNameVerbose', "{0} (Workspace)", this.getUriLabel(URI.file(join(dirname(workspace.configPath), workspaceName))));
|
||||
}
|
||||
|
||||
return localize('workspaceName', "{0} (Workspace)", workspaceName);
|
||||
}
|
||||
|
||||
getHostLabel(): string {
|
||||
if (this.windowService) {
|
||||
const authority = this.windowService.getConfiguration().remoteAuthority;
|
||||
if (authority) {
|
||||
const formatter = this.findFormatting(URI.from({ scheme: REMOTE_HOST_SCHEME, authority }));
|
||||
if (formatter && formatter.workspaceSuffix) {
|
||||
return formatter.workspaceSuffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
registerFormatter(formatter: ResourceLabelFormatter): IDisposable {
|
||||
this.formatters.push(formatter);
|
||||
this._onDidChangeFormatters.fire();
|
||||
|
||||
return {
|
||||
dispose: () => {
|
||||
this.formatters = this.formatters.filter(f => f !== formatter);
|
||||
this._onDidChangeFormatters.fire();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private formatUri(resource: URI, formatting: ResourceLabelFormatting, forceNoTildify?: boolean): string {
|
||||
let label = formatting.label.replace(labelMatchingRegexp, match => {
|
||||
switch (match) {
|
||||
case '${scheme}': return resource.scheme;
|
||||
case '${authority}': return resource.authority;
|
||||
case '${path}': return resource.path;
|
||||
default: return '';
|
||||
}
|
||||
});
|
||||
|
||||
// convert \c:\something => C:\something
|
||||
if (formatting.normalizeDriveLetter && hasDriveLetter(label)) {
|
||||
label = label.charAt(1).toUpperCase() + label.substr(2);
|
||||
}
|
||||
|
||||
if (formatting.tildify && !forceNoTildify) {
|
||||
label = tildify(label, this.environmentService.userHome);
|
||||
}
|
||||
if (formatting.authorityPrefix && resource.authority) {
|
||||
label = formatting.authorityPrefix + label;
|
||||
}
|
||||
|
||||
return label.replace(sepRegexp, formatting.separator);
|
||||
}
|
||||
}
|
||||
100
src/vs/workbench/services/label/test/label.test.ts
Normal file
100
src/vs/workbench/services/label/test/label.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { TestEnvironmentService, TestContextService, TestWindowService } from 'vs/workbench/test/workbenchTestServices';
|
||||
import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { nativeSep } from 'vs/base/common/paths';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
import { LabelService } from 'vs/workbench/services/label/common/labelService';
|
||||
|
||||
suite('URI Label', () => {
|
||||
|
||||
let labelService: LabelService;
|
||||
|
||||
setup(() => {
|
||||
labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestWindowService());
|
||||
});
|
||||
|
||||
test('file scheme', function () {
|
||||
labelService.registerFormatter({
|
||||
scheme: 'file',
|
||||
formatting: {
|
||||
label: '${path}',
|
||||
separator: nativeSep,
|
||||
tildify: !isWindows,
|
||||
normalizeDriveLetter: isWindows
|
||||
}
|
||||
});
|
||||
|
||||
const uri1 = TestWorkspace.folders[0].uri.with({ path: TestWorkspace.folders[0].uri.path.concat('/a/b/c/d') });
|
||||
assert.equal(labelService.getUriLabel(uri1, { relative: true }), isWindows ? 'a\\b\\c\\d' : 'a/b/c/d');
|
||||
assert.equal(labelService.getUriLabel(uri1, { relative: false }), isWindows ? 'C:\\testWorkspace\\a\\b\\c\\d' : '/testWorkspace/a/b/c/d');
|
||||
|
||||
const uri2 = URI.file('c:\\1/2/3');
|
||||
assert.equal(labelService.getUriLabel(uri2, { relative: false }), isWindows ? 'C:\\1\\2\\3' : '/c:\\1/2/3');
|
||||
});
|
||||
|
||||
test('custom scheme', function () {
|
||||
labelService.registerFormatter({
|
||||
scheme: 'vscode',
|
||||
formatting: {
|
||||
label: 'LABEL/${path}/${authority}/END',
|
||||
separator: '/',
|
||||
tildify: true,
|
||||
normalizeDriveLetter: true
|
||||
}
|
||||
});
|
||||
|
||||
const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5');
|
||||
assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'LABEL//1/2/3/4/5/microsoft.com/END');
|
||||
});
|
||||
|
||||
test('custom authority', function () {
|
||||
labelService.registerFormatter({
|
||||
scheme: 'vscode',
|
||||
authority: 'micro*',
|
||||
formatting: {
|
||||
label: 'LABEL/${path}/${authority}/END',
|
||||
separator: '/'
|
||||
}
|
||||
});
|
||||
|
||||
const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5');
|
||||
assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'LABEL//1/2/3/4/5/microsoft.com/END');
|
||||
});
|
||||
|
||||
test('mulitple authority', function () {
|
||||
labelService.registerFormatter({
|
||||
scheme: 'vscode',
|
||||
authority: 'not_matching_but_long',
|
||||
formatting: {
|
||||
label: 'first',
|
||||
separator: '/'
|
||||
}
|
||||
});
|
||||
labelService.registerFormatter({
|
||||
scheme: 'vscode',
|
||||
authority: 'microsof*',
|
||||
formatting: {
|
||||
label: 'second',
|
||||
separator: '/'
|
||||
}
|
||||
});
|
||||
labelService.registerFormatter({
|
||||
scheme: 'vscode',
|
||||
authority: 'mi*',
|
||||
formatting: {
|
||||
label: 'third',
|
||||
separator: '/'
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the most specific authority is picked
|
||||
const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5');
|
||||
assert.equal(labelService.getUriLabel(uri1, { relative: false }), 'second');
|
||||
});
|
||||
});
|
||||
@@ -27,61 +27,65 @@ export interface IRawLanguageExtensionPoint {
|
||||
configuration: string;
|
||||
}
|
||||
|
||||
export const languagesExtPoint: IExtensionPoint<IRawLanguageExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<IRawLanguageExtensionPoint[]>('languages', [], {
|
||||
description: nls.localize('vscode.extension.contributes.languages', 'Contributes language declarations.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { id: '${1:languageId}', aliases: ['${2:label}'], extensions: ['${3:extension}'], configuration: './language-configuration.json' } }],
|
||||
properties: {
|
||||
id: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.id', 'ID of the language.'),
|
||||
type: 'string'
|
||||
},
|
||||
aliases: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.aliases', 'Name aliases for the language.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
export const languagesExtPoint: IExtensionPoint<IRawLanguageExtensionPoint[]> = ExtensionsRegistry.registerExtensionPoint<IRawLanguageExtensionPoint[]>({
|
||||
isDynamic: true,
|
||||
extensionPoint: 'languages',
|
||||
jsonSchema: {
|
||||
description: nls.localize('vscode.extension.contributes.languages', 'Contributes language declarations.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { id: '${1:languageId}', aliases: ['${2:label}'], extensions: ['${3:extension}'], configuration: './language-configuration.json' } }],
|
||||
properties: {
|
||||
id: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.id', 'ID of the language.'),
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
extensions: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.extensions', 'File extensions associated to the language.'),
|
||||
default: ['.foo'],
|
||||
type: 'array',
|
||||
items: {
|
||||
},
|
||||
aliases: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.aliases', 'Name aliases for the language.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
extensions: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.extensions', 'File extensions associated to the language.'),
|
||||
default: ['.foo'],
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
filenames: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.filenames', 'File names associated to the language.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
filenamePatterns: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.filenamePatterns', 'File name glob patterns associated to the language.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
mimetypes: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.mimetypes', 'Mime types associated to the language.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
firstLine: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.firstLine', 'A regular expression matching the first line of a file of the language.'),
|
||||
type: 'string'
|
||||
},
|
||||
configuration: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.configuration', 'A relative path to a file containing configuration options for the language.'),
|
||||
type: 'string',
|
||||
default: './language-configuration.json'
|
||||
}
|
||||
},
|
||||
filenames: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.filenames', 'File names associated to the language.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
filenamePatterns: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.filenamePatterns', 'File name glob patterns associated to the language.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
mimetypes: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.mimetypes', 'Mime types associated to the language.'),
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
firstLine: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.firstLine', 'A regular expression matching the first line of a file of the language.'),
|
||||
type: 'string'
|
||||
},
|
||||
configuration: {
|
||||
description: nls.localize('vscode.extension.contributes.languages.configuration', 'A relative path to a file containing configuration options for the language.'),
|
||||
type: 'string',
|
||||
default: './language-configuration.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +137,7 @@ export class WorkbenchModeServiceImpl extends ModeServiceImpl {
|
||||
}
|
||||
}
|
||||
|
||||
ModesRegistry.registerLanguages(allValidLanguages);
|
||||
ModesRegistry.setDynamicLanguages(allValidLanguages);
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { INotificationService, INotification, INotificationHandle, Severity, NotificationMessage, INotificationActions, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification';
|
||||
import { INotificationsModel, NotificationsModel, ChoiceAction } from 'vs/workbench/common/notifications';
|
||||
import { dispose, Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { once } from 'vs/base/common/event';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
|
||||
export class NotificationService extends Disposable implements INotificationService {
|
||||
|
||||
@@ -90,7 +90,7 @@ export class NotificationService extends Disposable implements INotificationServ
|
||||
// Show notification with actions
|
||||
handle = this.notify({ severity, message, actions, sticky: options && options.sticky, silent: options && options.silent });
|
||||
|
||||
once(handle.onDidClose)(() => {
|
||||
Event.once(handle.onDidClose)(() => {
|
||||
|
||||
// Cleanup when notification gets disposed
|
||||
dispose(toDispose);
|
||||
|
||||
@@ -12,7 +12,7 @@ export const IPanelService = createDecorator<IPanelService>('panelService');
|
||||
export interface IPanelIdentifier {
|
||||
id: string;
|
||||
name: string;
|
||||
cssClass: string;
|
||||
cssClass?: string;
|
||||
}
|
||||
|
||||
export interface IPanelService {
|
||||
@@ -41,10 +41,4 @@ export interface IPanelService {
|
||||
* Returns pinned panels following the visual order
|
||||
*/
|
||||
getPinnedPanels(): IPanelIdentifier[];
|
||||
|
||||
/**
|
||||
* Enables or disables a panel. Disabled panels are completly hidden from UI.
|
||||
* By default all panels are enabled.
|
||||
*/
|
||||
setPanelEnablement(id: string, enabled: boolean): void;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,12 @@ export interface IPartService {
|
||||
*/
|
||||
getTitleBarOffset(): number;
|
||||
|
||||
/**
|
||||
*
|
||||
* Set editor area hidden or not
|
||||
*/
|
||||
setEditorHidden(hidden: boolean): void;
|
||||
|
||||
/**
|
||||
* Set sidebar hidden or not
|
||||
*/
|
||||
|
||||
@@ -33,7 +33,7 @@ import { IWorkspaceConfigurationService } from 'vs/workbench/services/configurat
|
||||
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { GroupDirection, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
|
||||
import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, getSettingsTargetName, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, SettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
|
||||
import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, getSettingsTargetName, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, SettingsEditorOptions, USE_SPLIT_JSON_SETTING } from 'vs/workbench/services/preferences/common/preferences';
|
||||
import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput';
|
||||
import { defaultKeybindingsContents, DefaultKeybindingsEditorModel, DefaultSettings, DefaultSettingsEditorModel, Settings2EditorModel, SettingsEditorModel, WorkspaceConfigurationEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels';
|
||||
|
||||
@@ -45,7 +45,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
|
||||
private lastOpenedSettingsInput: PreferencesEditorInput | null = null;
|
||||
|
||||
private readonly _onDispose: Emitter<void> = new Emitter<void>();
|
||||
private readonly _onDispose = new Emitter<void>();
|
||||
|
||||
private _defaultUserSettingsUriCounter = 0;
|
||||
private _defaultUserSettingsContentModel: DefaultSettings;
|
||||
@@ -55,21 +55,21 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
private _defaultFolderSettingsContentModel: DefaultSettings;
|
||||
|
||||
constructor(
|
||||
@IEditorService private editorService: IEditorService,
|
||||
@IEditorGroupsService private editorGroupService: IEditorGroupsService,
|
||||
@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,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IWorkspaceConfigurationService private readonly configurationService: IWorkspaceConfigurationService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@ITextModelService private readonly textModelResolverService: ITextModelService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IModelService private modelService: IModelService,
|
||||
@IJSONEditingService private jsonEditingService: IJSONEditingService,
|
||||
@IModeService private modeService: IModeService,
|
||||
@ILabelService private labelService: ILabelService
|
||||
@IModelService private readonly modelService: IModelService,
|
||||
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
|
||||
@IModeService private readonly modeService: IModeService,
|
||||
@ILabelService private readonly labelService: ILabelService
|
||||
) {
|
||||
super();
|
||||
// The default keybindings.json updates based on keyboard layouts, so here we make sure
|
||||
@@ -103,7 +103,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return this.getEditableSettingsURI(ConfigurationTarget.WORKSPACE_FOLDER, resource);
|
||||
}
|
||||
|
||||
resolveModel(uri: URI): Thenable<ITextModel> {
|
||||
resolveModel(uri: URI): Promise<ITextModel> {
|
||||
if (this.isDefaultSettingsResource(uri)) {
|
||||
|
||||
const target = this.getConfigurationTargetFromDefaultSettingsResource(uri);
|
||||
@@ -150,7 +150,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
createPreferencesEditorModel(uri: URI): Thenable<IPreferencesEditorModel<any>> {
|
||||
createPreferencesEditorModel(uri: URI): Promise<IPreferencesEditorModel<any>> {
|
||||
if (this.isDefaultSettingsResource(uri)) {
|
||||
return this.createDefaultSettingsEditorModel(uri);
|
||||
}
|
||||
@@ -171,15 +171,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return Promise.resolve<IPreferencesEditorModel<any>>(null);
|
||||
}
|
||||
|
||||
openRawDefaultSettings(): Thenable<IEditor> {
|
||||
openRawDefaultSettings(): Promise<IEditor> {
|
||||
return this.editorService.openEditor({ resource: this.defaultSettingsRawResource });
|
||||
}
|
||||
|
||||
openRawUserSettings(): Thenable<IEditor> {
|
||||
openRawUserSettings(): Promise<IEditor> {
|
||||
return this.editorService.openEditor({ resource: this.userSettingsResource });
|
||||
}
|
||||
|
||||
openSettings(jsonEditor?: boolean): Thenable<IEditor> {
|
||||
openSettings(jsonEditor?: boolean): Promise<IEditor> {
|
||||
jsonEditor = typeof jsonEditor === 'undefined' ?
|
||||
this.configurationService.getValue('workbench.settings.editor') === 'json' :
|
||||
jsonEditor;
|
||||
@@ -194,13 +194,13 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return this.openOrSwitchSettings(target, resource);
|
||||
}
|
||||
|
||||
private openSettings2(): Thenable<IEditor> {
|
||||
private openSettings2(): Promise<IEditor> {
|
||||
const input = this.settingsEditor2Input;
|
||||
return this.editorGroupService.activeGroup.openEditor(input)
|
||||
.then(() => this.editorGroupService.activeGroup.activeControl);
|
||||
}
|
||||
|
||||
openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Thenable<IEditor> {
|
||||
openGlobalSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise<IEditor> {
|
||||
jsonEditor = typeof jsonEditor === 'undefined' ?
|
||||
this.configurationService.getValue('workbench.settings.editor') === 'json' :
|
||||
jsonEditor;
|
||||
@@ -210,7 +210,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
this.openOrSwitchSettings2(ConfigurationTarget.USER, undefined, options, group);
|
||||
}
|
||||
|
||||
openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Thenable<IEditor> {
|
||||
openWorkspaceSettings(jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise<IEditor | null> {
|
||||
jsonEditor = typeof jsonEditor === 'undefined' ?
|
||||
this.configurationService.getValue('workbench.settings.editor') === 'json' :
|
||||
jsonEditor;
|
||||
@@ -225,7 +225,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
this.openOrSwitchSettings2(ConfigurationTarget.WORKSPACE, undefined, options, group);
|
||||
}
|
||||
|
||||
openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Thenable<IEditor> {
|
||||
openFolderSettings(folder: URI, jsonEditor?: boolean, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise<IEditor> {
|
||||
jsonEditor = typeof jsonEditor === 'undefined' ?
|
||||
this.configurationService.getValue('workbench.settings.editor') === 'json' :
|
||||
jsonEditor;
|
||||
@@ -235,7 +235,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
this.openOrSwitchSettings2(ConfigurationTarget.WORKSPACE_FOLDER, folder, options, group);
|
||||
}
|
||||
|
||||
switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): Thenable<void> {
|
||||
switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): Promise<void> {
|
||||
if (!jsonEditor) {
|
||||
return this.doOpenSettings2(target, resource).then(() => null);
|
||||
}
|
||||
@@ -248,7 +248,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
}
|
||||
}
|
||||
|
||||
openGlobalKeybindingSettings(textual: boolean): Thenable<void> {
|
||||
openGlobalKeybindingSettings(textual: boolean): Promise<void> {
|
||||
/* __GDPR__
|
||||
"openKeybindings" : {
|
||||
"textual" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
|
||||
@@ -268,9 +268,9 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return Promise.all([
|
||||
this.editorService.openEditor({ resource: this.defaultKeybindingsResource, options: { pinned: true, preserveFocus: true, revealIfOpened: true }, label: nls.localize('defaultKeybindings', "Default Keybindings"), description: '' }),
|
||||
this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true, revealIfOpened: true } }, sideEditorGroup.id)
|
||||
]).then(editors => void 0);
|
||||
]).then(editors => undefined);
|
||||
} else {
|
||||
return this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true, revealIfOpened: true } }).then(() => void 0);
|
||||
return this.editorService.openEditor({ resource: editableKeybindings, options: { pinned: true, revealIfOpened: true } }).then(() => undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -278,7 +278,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { pinned: true, revealIfOpened: true }).then(() => null);
|
||||
}
|
||||
|
||||
openDefaultKeybindingsFile(): Thenable<IEditor> {
|
||||
openDefaultKeybindingsFile(): Promise<IEditor> {
|
||||
return this.editorService.openEditor({ resource: this.defaultKeybindingsResource, label: nls.localize('defaultKeybindings', "Default Keybindings") });
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
}));
|
||||
}
|
||||
|
||||
private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Thenable<IEditor> {
|
||||
private openOrSwitchSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Promise<IEditor> {
|
||||
const editorInput = this.getActiveSettingsEditorInput(group);
|
||||
if (editorInput && editorInput.master.getResource().fsPath !== resource.fsPath) {
|
||||
return this.doSwitchSettings(configurationTarget, resource, editorInput, group, options);
|
||||
@@ -308,12 +308,18 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return this.doOpenSettings(configurationTarget, resource, options, group);
|
||||
}
|
||||
|
||||
private openOrSwitchSettings2(configurationTarget: ConfigurationTarget, folderUri?: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Thenable<IEditor> {
|
||||
private openOrSwitchSettings2(configurationTarget: ConfigurationTarget, folderUri?: URI, options?: ISettingsEditorOptions, group: IEditorGroup = this.editorGroupService.activeGroup): Promise<IEditor> {
|
||||
return this.doOpenSettings2(configurationTarget, folderUri, options, group);
|
||||
}
|
||||
|
||||
private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group?: IEditorGroup): Thenable<IEditor> {
|
||||
private doOpenSettings(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise<IEditor> {
|
||||
const openSplitJSON = !!this.configurationService.getValue(USE_SPLIT_JSON_SETTING);
|
||||
if (openSplitJSON) {
|
||||
return this.doOpenSplitJSON(configurationTarget, resource, options, group);
|
||||
}
|
||||
|
||||
const openDefaultSettings = !!this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING);
|
||||
|
||||
return this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource)
|
||||
.then(editableSettingsEditorInput => {
|
||||
if (!options) {
|
||||
@@ -323,12 +329,31 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
}
|
||||
|
||||
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, SettingsEditorOptions.create(options), group);
|
||||
const activeEditorGroup = this.editorGroupService.activeGroup;
|
||||
const sideEditorGroup = this.editorGroupService.addGroup(activeEditorGroup.id, GroupDirection.RIGHT);
|
||||
return Promise.all([
|
||||
this.editorService.openEditor({ resource: this.defaultSettingsRawResource, options: { pinned: true, preserveFocus: true, revealIfOpened: true }, label: nls.localize('defaultSettings', "Default Settings"), description: '' }),
|
||||
this.editorService.openEditor(editableSettingsEditorInput, { pinned: true, revealIfOpened: true }, sideEditorGroup.id)
|
||||
]).then(() => null);
|
||||
} else {
|
||||
return this.editorService.openEditor(editableSettingsEditorInput, SettingsEditorOptions.create(options), group);
|
||||
}
|
||||
return this.editorService.openEditor(editableSettingsEditorInput, SettingsEditorOptions.create(options), group);
|
||||
});
|
||||
}
|
||||
|
||||
private doOpenSplitJSON(configurationTarget: ConfigurationTarget, resource: URI, options?: ISettingsEditorOptions, group?: IEditorGroup): Promise<IEditor> {
|
||||
return this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource)
|
||||
.then(editableSettingsEditorInput => {
|
||||
if (!options) {
|
||||
options = { pinned: true };
|
||||
} else {
|
||||
options = assign(options, { pinned: true });
|
||||
}
|
||||
|
||||
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, SettingsEditorOptions.create(options), group);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -336,7 +361,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return this.instantiationService.createInstance(Settings2EditorModel, this.getDefaultSettings(ConfigurationTarget.USER));
|
||||
}
|
||||
|
||||
private doOpenSettings2(target: ConfigurationTarget, folderUri: URI | undefined, options?: IEditorOptions, group?: IEditorGroup): Thenable<IEditor> {
|
||||
private doOpenSettings2(target: ConfigurationTarget, folderUri: URI | undefined, options?: IEditorOptions, group?: IEditorGroup): Promise<IEditor> {
|
||||
const input = this.settingsEditor2Input;
|
||||
const settingsOptions: ISettingsEditorOptions = {
|
||||
...options,
|
||||
@@ -347,7 +372,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return this.editorService.openEditor(input, SettingsEditorOptions.create(settingsOptions), group);
|
||||
}
|
||||
|
||||
private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, group: IEditorGroup, options?: ISettingsEditorOptions): Thenable<IEditor> {
|
||||
private doSwitchSettings(target: ConfigurationTarget, resource: URI, input: PreferencesEditorInput, group: IEditorGroup, options?: ISettingsEditorOptions): Promise<IEditor> {
|
||||
return this.getOrCreateEditableSettingsEditorInput(target, this.getEditableSettingsURI(target, resource))
|
||||
.then(toInput => {
|
||||
return group.openEditor(input).then(() => {
|
||||
@@ -422,12 +447,12 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return target === ConfigurationTarget.WORKSPACE_FOLDER ? nls.localize('folderSettingsName', "{0} (Folder Settings)", name) : name;
|
||||
}
|
||||
|
||||
private getOrCreateEditableSettingsEditorInput(target: ConfigurationTarget, resource: URI): Thenable<EditorInput> {
|
||||
private getOrCreateEditableSettingsEditorInput(target: ConfigurationTarget, resource: URI): Promise<EditorInput> {
|
||||
return this.createSettingsIfNotExists(target, resource)
|
||||
.then(() => <EditorInput>this.editorService.createInput({ resource }));
|
||||
}
|
||||
|
||||
private createEditableSettingsEditorModel(configurationTarget: ConfigurationTarget, resource: URI): Thenable<SettingsEditorModel> {
|
||||
private createEditableSettingsEditorModel(configurationTarget: ConfigurationTarget, resource: URI): Promise<SettingsEditorModel> {
|
||||
const settingsUri = this.getEditableSettingsURI(configurationTarget, resource);
|
||||
if (settingsUri) {
|
||||
const workspace = this.contextService.getWorkspace();
|
||||
@@ -441,7 +466,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return Promise.resolve<SettingsEditorModel>(null);
|
||||
}
|
||||
|
||||
private createDefaultSettingsEditorModel(defaultSettingsUri: URI): Thenable<DefaultSettingsEditorModel> {
|
||||
private createDefaultSettingsEditorModel(defaultSettingsUri: URI): Promise<DefaultSettingsEditorModel> {
|
||||
return this.textModelResolverService.createModelReference(defaultSettingsUri)
|
||||
.then(reference => {
|
||||
const target = this.getConfigurationTargetFromDefaultSettingsResource(defaultSettingsUri);
|
||||
@@ -485,17 +510,17 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return null;
|
||||
}
|
||||
|
||||
private createSettingsIfNotExists(target: ConfigurationTarget, resource: URI): Thenable<void> {
|
||||
private createSettingsIfNotExists(target: ConfigurationTarget, resource: URI): Promise<void> {
|
||||
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE && target === ConfigurationTarget.WORKSPACE) {
|
||||
const workspaceConfig = this.contextService.getWorkspace().configuration;
|
||||
if (!workspaceConfig) {
|
||||
return Promise.resolve(null);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return this.fileService.resolveContent(workspaceConfig)
|
||||
.then(content => {
|
||||
if (Object.keys(parse(content.value)).indexOf('settings') === -1) {
|
||||
return this.jsonEditingService.write(resource, { key: 'settings', value: {} }, true).then(null, () => { });
|
||||
return this.jsonEditingService.write(resource, { key: 'settings', value: {} }, true).then(undefined, () => { });
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -503,10 +528,10 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
return this.createIfNotExists(resource, emptyEditableSettingsContent).then(() => { });
|
||||
}
|
||||
|
||||
private createIfNotExists(resource: URI, contents: string): Thenable<any> {
|
||||
return this.fileService.resolveContent(resource, { acceptTextOnly: true }).then(null, error => {
|
||||
private createIfNotExists(resource: URI, contents: string): Promise<any> {
|
||||
return this.fileService.resolveContent(resource, { acceptTextOnly: true }).then(undefined, error => {
|
||||
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
|
||||
return this.fileService.updateContent(resource, contents).then(null, error => {
|
||||
return this.fileService.updateContent(resource, contents).then(undefined, error => {
|
||||
return Promise.reject(new Error(nls.localize('fail.createSettings', "Unable to create '{0}' ({1}).", this.labelService.getUriLabel(resource, { relative: true }), error)));
|
||||
});
|
||||
}
|
||||
@@ -531,7 +556,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
|
||||
];
|
||||
}
|
||||
|
||||
private addLanguageOverrideEntry(language: string, settingsModel: IPreferencesEditorModel<ISetting>, codeEditor: ICodeEditor): Thenable<IPosition> {
|
||||
private addLanguageOverrideEntry(language: string, settingsModel: IPreferencesEditorModel<ISetting>, codeEditor: ICodeEditor): Promise<IPosition> {
|
||||
const languageKey = `[${language}]`;
|
||||
let setting = settingsModel.getPreference(languageKey);
|
||||
const model = codeEditor.getModel();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user